Русский
Русский
English
Статистика
Реклама

Elixir

5 альтернатив Node.js и есть ли в этом смысл

29.03.2021 16:17:23 | Автор: admin


Node.js не безупречный продукт, у него есть недостатки (использование JS?), некоторые из них тянулись еще с раннего этапа разработки из-за ошибочных решений, принятых Райаном Далем, о которых немного вспомнили в статье Как создатель node.js сам разочаровался в нем. Как это часто бывает, в некоторых случаях альтернативу этой системе найти сложно или невозможно, особенно в секторе энтерпрайза. Но если не требуется поддержки большого легаси-кода, который еще много лет будут снабжать работой программистов, то можно взглянуть в сторону других решений. Про убийцу Node.js можно почитать в статье, указанной выше, о некоторых других будет рассказано в этой статье.

ASP.NET




Старый друг лучше новых двух. ASP.NET имеет длинную историю. Технология Active Server Pages, которая легла в основу сервисов ASP.NET, была разработана аж в конце прошлого века. Конечно, продолжительность разработки это не всегда хорошо, но долгая жизнь продукта говорит о многом. Хоть у ASP.NET совсем другой принцип работы, это не просто окружение для запуска программного кода и подключения модулей, это платформа для создания веб-сервисов; она часто используется для решения похожих с Node.js задач.

Если Node.js детище небольшой команды и в самом базовом виде является средой запуска программ на JS вне браузера с доступом к вводу/выводу, то ASP.NET типичный продукт огромной корпорации. В нем изначально содержится большое количество дефолтных библиотек, которые сразу позволяют начать разработку. Node развивается благодаря сообществу разработчиков, и потому систему надо сначала обвесить всеми необходимыми модулями, которых просто невероятное количество. Простейший Hello world! на Node притащит в систему несколько тысяч файлов. Причем, количество не всегда перерастает в качество, иногда создается впечатление, будто разработчики даже i++ готовы запихать в отдельный модуль, который потом будет скачан миллион раз, а внезапное обновление или удаление этого пакета вызовет серьезные проблемы совместимости или обрушит npm. В ASP за безопасностью и обновлениями следит экосистема Microsoft, библиотеки, написанные ей и другими крупными компаниями, тщательно проверены на ошибки и проблемы с совместимостью

Несмотря на многие отличия, одним из главных является разный подход к распределению вычислений. В Node.js все выполнялось в одном потоке, но с помощью асинхронного ввода/вывода. ASP изначально работала в многопоточном режиме и синхронном вводе/выводе. Первый вариант показывает большую производительность при работе сервисов требующих очень интенсивный обмен, но ценой некоторого усложнения кода. В последних версиях продуктов эти отличия сглаживаются, в Node.js и ASP.NET применяется паттерн async\await.

JS очень сильно вырос за время своего развития, но все равно остается языком без строгой типизации и проигрывает C#, который изначально разрабатывался под прямым влиянием C++, с первых дней имевшем строгую типизацию и ООП. Следствием этого является то, что C# мощнее и более цельнее, ему не требуются надстройки вроде TS. Но зато JS за счет своей простоты более востребован при программировании микросервисов, в котором не требуются сложные возможности C#.

Кроме различий используемых языков программирования, у этих платформ разная идеология. В ASP.NET многое предопределено, он предлагает все готовое, начиная с файловой структуры, тогда как, начиная работать с Node.js, приходится выбирать все самому, что дает большую гибкость, но усложняет разработку. Зато в ASP.NET есть мощные инструменты отладки, тестирования и рефакторинга.

Единственное в чем ASP.NET серьезно уступает Node.js это простота развертывания. Node нужен только движок и прокси-сервер или Docker. Платформу можно настроить как самому, так и взять готовый образ VPS, который есть практически у любого хостинг-провайдера. Для ASP, несмотря на кроссплатформенность, все не так легко, а готовые образы для серверов есть только у самых крупных игроков уровня Azure.

Так ли нужен JavaScript?


Node.js создавалась как среда выполнения для JS, потому что это простой и доступный язык программирования, с помощью которого легко удалось реализовать одновременное выполнение нескольких сценариев для двустороннего обмена данными веб-приложения между браузером и сервером. ASP.NET тоже создавался для написания веб-приложений, только с помощью других методов. Тот же Deno состоит из нескольких слоев над виртуальной машиной, которая непосредственно работает с ресурсами сервера. Но ведь не обязательно использовать сложные обертки из языков программирования, которые не исполняются непосредственно на сервере и требуют виртуальных машин.

Go




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

У разработчиков было целью не создать лучшую версию C++, а сделать новый язык более понятным, основываясь на своем опыте программирования. Go был публично анонсирован в 2009 году, довольно быстро поднялся почти в топ-10 языков программирования (13 место в начале 2012 года). Сам Раймон Даль расхваливал его в выражениях наподобие: Зачем Node.js, если есть такой прекрасный язык как Go?. Но новизна прошла, и язык был почти забыт, пока не оказалось, что его средства распараллеливания хорошо подходят для разработки микросервисов в веб-приложениях. Это вернуло языку былую популярность.

Для Go написано несколько веб-фреймворков, хотя пользоваться ими не обязательно, потом что из коробки у него есть весь функционал, необходимый для программирования веб-приложений. Изначально Deno был написан именно на Go.

Ruby




Снова вспомним Deno, а именно то, что сейчас его код написан на Ruby.
По сравнению с тем же Go, это не самый новый язык, его релиз был в 1995 году. В отличии от Go, разработкой занимался одиночка-энтузиаст Юкихиро Мацумото, который загорелся созданием объектно-ориентированного, интерпретируемого языка, который был бы лучше чем Pyton. Популярность язык завоевал далеко не сразу, не в последнюю очередь потому, что первую пару лет его документация была только на японском. Книги на английском языке вышли только в начале 2000-х, а признание пришло после выхода веб-фреймворка Ruby on Rails в 2005 году, который быстро стал популярным, особенно когда Apple заявила в 2007 году, что будет поставлять его в составе Mac OS X 10.5.

Как и во всех интерпретируемых языках, скорость Ruby была сравнительно небольшой, что сильно сказывалось в работе веб-приложений, написанных на Ruby on Rails. Зная эту проблему, автор стал разрабатывать версию языка, которая потеряла бы обратную совместимость, но стала бы выполняться гораздо быстрее, что и было им сделано в 2009 году. Скорость выросла очень существенно и сравнялась с веб-приложениями работающими под .NET и JVM. Благодаря этому резко выросла популярность фреймворка. В 2018 году автор Ruby выпустил версию 2,6, в которой реализована динамическая компиляция, что еще сильнее ускоряет работу приложений.
До сих пор язык Ruby, плотно ассоциируется именно с фреймворком Ruby on Rails.

Elixir




Erlang еще один язык программирования, созданный большой корпорацией. На этот раз постарались шведы из Ericsson. В 1986 году никто не думал о разработке веб-приложений, интернет был еще в проекте, а язык создавался для работы систем в реальном режиме времени. Долгое время Erlang был практически неизвестен за пределами компании, которая использовала его для своих внутренних нужд, пока в 1998 году руководство не решило прекратить поддержку собственного языка и запретило его использование, обязав разработчиков перейти на Java. Это привело к тому, что язык выпустили под открытой лицензией, и он стал распространяться за пределами компании, которая его разработала. Долгое время он был интересен только небольшому кругу ученых, пока не оказалось, что его концепция хорошо подходит для разработки под многопроцессорную архитектуру.

В 2012 году Хосе Валим, один из разработчиков Ruby on Rails, загорелся идеей создать язык программирования для высоконагруженных систем и больших веб-сайтов. В итоге был разработан Elixir, функциональный язык программирования, компилируемый в байт-код для виртуальной машины Erlang (BEAM). Благодаря тому, что в основе языка лежит Erlang, который разрабатывался для программирования коммуникационного оборудования, Elixir получил уникальные свойства: отказоустойчивость, горячую замену кода (изменение или откат кода работающей программы, без ее перезапуска) и возможность работы в реальном режиме времени. Эти свойства позволяют создавать надежные высоконагруженные системы, поддерживающие как горизонтальное, так и вертикальное масштабирование, работающие в 5-10 раз быстрее, чем аналогичные приложения написанные на интерпретируемых языках (PHP, Ruby, Python). Место работы создателя повлияло на то, что в языке используется Ruby-подобный синтаксис, и его легко освоить тем, кто пользуется Ruby on Rails.

На данный момент Elixir используется в таких известных компаниях, как Discord, Square Enix, PepsiCo и Sketch.

А если, все-таки JavaScript?


Но если осваивать новый язык программирования нецелесообразно, как и тащить на сервер всю Ноду? Можно посмотреть на другие среды выполнения JS.



RingoJS это многопоточная платформа, построенная на JVM и оптимизированная для работы на серверах. Для интерпретации кода JS используется движок Mozilla Rhino, имеющий внушительную историю. Его начали разрабатывать в 1997 году, еще во времена Netscape, позже проект передали Mozilla Foundation и выложили в open source.

Приложения RingoJS могут быть развернуты на любой платформе под управлением Linux, вплоть до Raspberry Pi, или поверх облачных платформ типа Google App Engine. Модульная система RingoJS основана на CommonJS, можно даже использовать некоторые модули от Node.js. Также движок Ringo позволяет интегрировать библиотеки написанные на Java.



PurpleJS это еще один простой JS-фреймворк запускаемый на JVM. В качестве движка JS используется Nashorn (тоже носорог), разрабатываемый Oracle. Нельзя сказать, что это полноценная замена Node, потому что фреймворк не использует асинхронный режим, а Nashorn не имеет поддержки CommonJS, зато он очень легкий и не требует перезапускать сервис после изменения кода.



Vert.x это уже не фреймворк, а мультиязычный набор инструментов, позволяющий создавать полностью асинхронные реактивные веб-приложения, микросервисы и сетевые утилиты запускаемые на JVM. Поддерживаемые языки не ограничиваются только Java и JS, кроме них можно писать приложения на Groovy, Ruby, Scala и Kotlin, поддерживается модульная система с централизованным репозиторием, Vert.x легко расширяется и масштабируется.

Забавно, что проект сначала назывался Node.x, потом его переименовали, чтобы избежать юридических проблем, потому что цели при создании были такие же, как у Node.js, и Тим Фокс (создатель Vert.x) говорил, что вдохновлялся успехом Дали и хотел сделать Node.js на JVM.

Если альтернативы не впечатляют и node.js остается вашей любовью



Можно брать преднастроенный сервер сразу с node.js из

Подробнее..

Затерянные в тумане, или Увлекательные приключения в мире АПР

09.11.2020 04:11:53 | Автор: admin

- А интересно, - подумал Ёжик, - если Лошадь ляжет спать, она захлебнется в тумане?

И ещё Ёжик думал о Лошади. Как она там, в тумане?..

* АПР - аэропоника с пневматическим распылением

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

Вышло ли из этого что-то и что конкретно - под катом. Сразу предупрежу: статья огромная, потому что материала много. И очень много фото.

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

Само собой, речи о выращивании земле не шло вообще: с детства не люблю в ней копаться. Сам я о высоких технологиях в сельском хозяйстве даже если и слышал, то краем уха. Так что я начал поиски с того, что уже использовалось - т.е. с туманогенерации, она же fogponics. Довольно быстро выяснилось, что опытные хай тек агрономы эту технологию не приветствуют из-за ряда её неприятных особенностей (как реальных, так и теоретических). Однако аэропоника (а туманопоника - как раз её часть) в целом показалась очень интересной, поэтому я начал двигаться по этому пути.

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

Вообще, что такое аэропоника? Это способ выращивания растений "в воздухе", при котором корни просто висят в пустом пространстве, а питательные вещества доставляются в виде спрея. Глобально аэропонику делят на два направления: это LPA, low-pressure aeroponics, или аэропоника низкого давления, и HPA, high pressure aeroponics, что, как вы уже, наверное, догадались, переводится как аэропоника высокого давления.

Лично с моей точки зрения LPA - это как бы и не аэропоника вовсе. Это, скорее, какой-то аналог NFT-гидропоники (nutrient film technique, техника питательного слоя): корни спрыскиваются крупными каплями питательного раствора или он вовсе подаётся сверху, а далее капает вниз, попадая на корни и создавая на них плёнку воды. Большое количество проектов с башнями для выращивания используют именно такую технологию.

А вот HPA - это уже настоящая аэропоника: питательный раствор периодически, по таймеру, подаётся под высоким давлением на сопла, где превращается в мелкий спрей, в идеале - с размером капли в районе 50 микрон, и, соответственно, этим спреем сбрызгиваются корни. И поначалу я решил идти именно этим путём.

Компоненты

Система на базе HPA состоит из следующих компонентов: насоса высокого давления (6-12 бар), труб для подачи раствора (которые должны это давление выдерживать), самих распылительных форсунок, гидроаккумулятора, реле давления, редуктора давления и электроклапанов (для уменьшения размера статьи я не буду тут рассказывать, зачем обязательно нужен гидроаккумулятор и электроклапаны, если возникнут вопросы - объясню в комментариях). По этому набору сразу можно понять, что в системе есть тонкие места:

  • гидроаккумулятор, по сути - бомба. Да, они надёжны, но если всё-таки рванёт - из-за воды мало не покажется;

  • и трубы, которые должны без протечек постоянно держать в себе 5-8 бар давления (к форсункам жидкость нужно подавать под давлением в 5.5-6+ бар).

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

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

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

...к несколько более экзотическому способу формирования спрея. Это AAA, air-assisted aeroponics (или иначе air-atomized aeroponics), т.е. аэропоника с пневматическим распылением. В этом случае для распыления используется энергия сжатого воздуха, к форсунке идёт две линии: одна с жидкостью, вторая - с воздухом, причём и там, и там гораздо более низкие давления (забегая вперёд: для получения тумана нужного качества я использую 1.5 бар давление раствора и 2.25 бар давление воздуха).

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

Набор компонентов для АПР несколько иной, конечно. Самая сложная часть, которая добавляется - это воздушный компрессор. Тут могут быть разные варианты, но я остановился на безмаслянном компрессоре с ресивером в 24 литра. Его номинальная производительность - 160 литров\мин, мощность - 1100W, заявленный уровень шума - 54 dBa. Повёлся на уверения производителя о его - компрессора - бесшумности, что оказалось не совсем правдой (я даже снял ролик на эту тему).

Чтобы упростить систему я решил отказаться от отдельного гидроаккумулятора и помпы, и тут используя воздух для создания давления жидкости. Для этого я в ближайшем пивном магазине купил б\у ПЭТ-кегу на 30 литров со съемным фитингом, а в конторе, которая занимается поставками оборудования для пивных - б\у заборную головку. В результате я заливаю питательный раствор в кегу, закручиваю фитинг, надеваю головку - и вуаля, туда подаётся воздух, нагнетая требуемое давление, а из другого отверстия заборной головки вытекает уже раствор.

Но компрессор - не самый дорогой элемент системы. Как оказалось, пневматические форсунки недёшевы. Набор для одной форсунки стоит $150 и выше - в зависимости от комплектации. Но всегда есть варианты...

Сборка

Итак, я потратил пару месяцев на то, чтобы прикинуть, что же мне нужно для сборки системы, и заказать компоненты. В качестве форсунок я решил попробовать использовать китайские сопла с AliExpress. Там же я заказал регуляторы давления, соленоиды и кучу электроники.

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

Фоточки процесса производства установки. ОЧЕНЬ МНОГО фоточек.

П

Бизиборд для взрослыхБизиборд для взрослых
Первая версияПервая версия

Я делал тогда небольшое видео с поверхностным обзором, а поскольку общался на американских форумах, то и озвучка там на плохом английском.

Первая версия предполагала основной редуктор давления на входе вместе с фильтром воздуха и устройством для осушения, два прецизионных редуктора давления от 0 до 2 бар (я на тот момент полагал, что смогу обойтись давлениями до 1 бар), два соленоида 12В, управляемых контроллером на базе esp32, а так же одно сопло.

На тот момент я уже получил электронику и даже написал тестовую прошивку, чтобы проверить свою идею с внешней панелью управления и микроконтроллером, который должен был общаться с панелью по MQTT-протоколу, о чём тоже снял видео (опять же, на плохом английском, и смотреть его нужно на скорости минимум x1.5).

Кстати, я не упомянул и о ещё одном компоненте - о свете. Свет - штука очень важная. Я бы сказал, что самая важная или одна из самых важных. Свет я заказывал на Alibaba, к сожалению, этого предложения больше нет, но это светодиодный светильник регулируемой мощностью 240Вт на диодах полного спектра Samsung lm301b с температурой 3000К с добавлением нескольких LED с длиной волны 660 nm. Светит очень ярко.

Немного о Свете (зачёркнуто) о свете

Помимо этого пришлось спаять и собрать какое-то количество электроники в процессе. Как-то так выглядит

Метод проб и ошибок

Но вот дальше всё пошло немного не так.

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

Вообще, это были копии сопел Spraying Systems, и я попытался было купить оригиналы официально, но ценник в $150 за штуку меня отпугнул. Однако всегда есть варианты: на eBay я нашёл нужное предложение, в результате четыре набора головок обошлись мне в $33, включая доставку, и я установил их на китайские базы.

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

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

К тому моменту я уже заказал саженцы, которые планировал высаживать - 30 кустов двух сортов: Альбион и Эви-2. И они даже приехали - где-то 20 марта, а буквально через пару дней в стране ввели карантин.

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

В результате у меня получилось два компонента.

Первый - это основная панель управления, в которой я описываю расписание работы впрыска (а в дальнейшем буду управлять и другими инструментами типа датчиков температуры и т.п.). Написана она на Elixir + Phoenix Framework + Phoenix LiveView и запускается на Raspberry Pi3, к которому подключены твердотельные реле, управляющие соленоидами. Реализовано всё на базе проекта Nerves, причём есть обновления софта OTA: достаточно загрузить новую версию прошивки на NervesHub, как устройство это обнаружит, обновится и перезапустится. Ссылку на GitHub я даю, но использовать проект пока малореально: нужно дописывать, ибо реализован только самый основной функционал, без которого ничего работать не могло. Ну, и UI/UX оставляет очень много места для улучшений.

Появление второго компонента связано с тем, что, во-первых, необходимо управлять светом, а кроме того оказалось, что реле включения компрессора срабатывает при падении давления ниже 6 бар, а выключается - при достижении 8. И изменить эту настройку нельзя, иначе потеряешь гарантию. Гарантию терять мне не хотелось, но и слышать работающий компрессор желательно было пореже. Поэтому я сварганил из говна и палок esp32, LCD-экранчика, россыпи резисторов, телефонных разъемов, твердотельных реле, корпуса накладной розетки на два гнезда и датчиков давления контроллер розеток. Контроллер управляет розеткой, к которой подключён компрессор, исходя из показаний датчика давления (кстати, их можно подключить до 5 штук), кроме того, розетка включается не дольше, чем на 5 минут, а затем выключается минимум на 5 минут (это из правил пользования компрессором).

Написано всё на C и родном esp-idf, проект тоже лежит на GitHub. Правда, и тут не всё реализовано программно... ну, вы поняли. Из интересного: многопоточность на C я до этого никогда не использовал, да и вообще это мой первый опыт программирования микроконтроллеров. Не знаю, насколько опыт успешен, но ни разу ничего не зависло и не сломалось. Обновление прошивки по воздуху (OTA) в планах.

Если будет интерес, могу об этих компонентах рассказать подробнее.

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

Питательный раствор

Тут я тоже долго изучал альтернативы. Думал и о готовых растворах, и о том, чтобы делать их самому. В конце концов решил рискнуть: сначала сделал смесь из удобрений, но там получалось всё настолько неудобно, сложно и с неопределённым результатом, что я нашёл поставщика хим. реактивов, купил чистые вещества и постепенно пришёл к нынешнему трёхкомпонентному концентрированному составу. К сожалению, я не знаю, насколько он хорош: хотелось бы сделать анализ листвы, чтобы проверить, чего хватает\не хватает, но пока не понимаю, где и как это произвести. Несколько раз своим неоптимальным составом и неправильной кислотностью я чуть не убил кусты, но обошлось.

Сам раствор я замешиваю на дистиллированной воде, которую покупаю. Для уверенности в составе повторно не используется раствор, сразу утилизируется. Кстати, так как это аэропоника, я использую раствор с EC в районе 400 mS/cm3 (TDS - ~200 ppm по шкале 0.5). Причём есть подозрение, что и этого многовато. Для сравнения - TDS водопроводной воды у меня около 130 ppm.

Выращивание

Несмотря на все задержки в какой-то момент нужно было уже посадить растения, иначе это всё теряло смысл. Я боялся, что мои саженцы, который к тому моменту 2.5 месяца провели в холодильнике (причём не при требуемых -1.5..-2 градуса, а при +4), померли. Но нет. Они, конечно, сильно ослабли, и по факту большая их часть всё-таки не выжила, но не все.

Запустил я процесс 16 июня, если мне не изменяет память.

Выглядели они, конечно, слабоватоВыглядели они, конечно, слабовато

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

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

Как я уже сказал, из тридцати кустов фриго выжило буквально 6 штук. И даже из этих шести здоровыми выглядели не все.

В какой-то момент я даже решил, что нужно попробовать попробовать внедрить в систему обычные растения из огорода, благо земляничный сезон уже заканчивался и можно было без вреда для урожая их усами размножать. Вот так выглядели высаженные растения (strawberry nursery я опущу - и так слишком много фото):

Но в какой-то момент я увидел первый цветок! Это было воодушевляюще! А потом цветы пошли валом. А следом за цветами появилась и первая ягода.

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

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

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

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

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

Подробнее..

Отправляем SMS из ErlangElixir. Короткая инструкция

30.12.2020 10:17:29 | Автор: admin


Photo by Science in HD


Если вам когда-либо приходилось решать задачу отправки SMS из кода вашего приложения, скорее всего, вы использовали готовое REST API поставщика дополнительных услуг. Но что происходит после того, как поставщик получит ваш запрос? Какие протоколы используются и какой путь проходит текст сообщения, прежде чем оказаться на экране мобильного терминала пользователя?


В этой статье вы найдёте:


  1. Немного теории и терминологии SMPP-протокола: SMSC, ESME, PDU, MO/MT SM.
  2. Краткий обзор существующих библиотек для работы с SMPP в Erlang/Elixir.
  3. Пример реализации асинхронного клиента при помощи библиотеки SMPPEX. Возможно, он будет полезен тем, кто ещё не использовал Elixir-библиотеки в Erlang-проектах.
  4. Информацию по обработке deliver_sm, MO SM.

Чего тут точно нет, так это информации по отправке коротких сообщений через SIGTRAN.


Определимся с терминами и понятиями


Прежде чем погружаться в протоколы и код, предлагаю разобраться в терминологии. Если быть придирчивым к определениям, то отправить SMS невозможно. Вспоминается момент из Джентльменов удачи: Кто ж его отправит он же сервис! SMS акроним от short message service, что на русский переводится как сервис/служба коротких сообщений. Если возвращаться к шутке, то отправляем мы SM, т. е. короткие сообщения, используя SMS сервис коротких сообщений.


У каждого оператора мобильной связи есть компонент, отвечающий за работу службы коротких сообщений. Это так называемый SMS-центр, он же SMSC, он же SMS-SC. Его задачами являются хранение, передача, конвертация и доставка SM-сообщений. Наиболее распространенным внешним протоколом взаимодействия с SMSC является SMPP. SMPP клиент-серверный протокол-комбайн, отвечающий за обмен короткими сообщениями в одноранговой сети. Источником SM могут быть устройства и приложения. В терминологии SMPP их называют ESME.


Давайте ответим на вопросы в начале статьи. Итак, ваше сообщение по REST API или SMPP попало к поставщику услуг, у которого заключён договор с одним или несколькими операторами связи или другими посредниками. Сервер поставщика подключается к SMSC и отправляет по SMPP ваше SM, затем получает отчёт о доставке или ответное SM. В процессе обработки SM могут проходить через маршрутизаторы RE. SMSC, сходив в HLR, узнает местоположение абонента и доставит SM абоненту. Общая картина и понимание проблемы, надеюсь, у вас появились. Давайте погрузимся в протокольные тонкости.


SMPP


Выше я сказал, что SMPP протокол-комбайн. Подобный эвфемизм я позволил себе из-за того, что SMPP применим не только для организации обмена SMS, с его помощью можно организовать различные сервисы: ESM, голосовой почты, уведомлений, сотового радиовещания, WAP, USSD и прочие. Весь обмен происходит с помощью пар запрос-ответ. Их называют PDU блоками данных или пакетами.


Инициализация подключения


Перед началом обмена мы должны указать, в каком направлении будет работать наше подключение. За это отвечают соответствующие команды:


  • bind_transmitter клиент может только отправлять запросы на сервер;
  • bind_receiver клиент только получает ответы от сервера;
  • bind_transceiver клиент работает в обоих направлениях, этот режим появился в SMPPv3.4.

Безопасность


При выполнении команды привязки мы должны передать параметры безопасности для идентификации нашего ESME: system_id, system_type и password.


SMPP в экосистеме OTP


Недавно у хорошего друга возник вопрос по работе с SMPP в Erlang. Собственно, благодаря этому и родился этот текст.


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



Но есть нюанс Найти адекватную реализацию оказалось непросто.


Наверняка все, кто пытался серьёзно работать с Erlang, знают про его недостаток, связанный с ограниченным выбором библиотек. С SMPP такая же история в OTP нет штатной поддержки этого протокола, а на первой странице выдачи Гугла творится что-то странное:


  • esmpp библиотека со странным API и отсутствующим сообществом;
  • древний OSERL проект стартовал 11 лет назад, последний коммит сделали более 5 лет назад;
  • неподдерживаемый smpp34 последний коммит был более 10 лет назад;
  • куча вопросов вида Какую библиотеку/клиента использовать для SMPP? на тематических форумах.

Лично я бы загрустил от такого разнообразия существующих решений. Особенно, когда хочется асинхронного режима, адекватной поддержки SMPP 3.4 и возможности написать как клиент, так и сервер. Но на помощь приходит Elixir и библиотека SMPPEX.


SMPPEX


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


От слов к делу


Сначала можно ознакомиться с примерами синхронного и асинхронного клиента в документации. Затем можно перейти к более сложным вещам в контексте использования SMPPEX в Erlang-проекте.


Для иллюстрации возможностей библиотеки возьмём простой сценарий:


  1. Поднять линк.
  2. Отправить сообщение.
  3. Дождаться отчёта о доставке либо обработать входящие сообщения.

Придумаем дополнительные требования. Допустим, мы хотим отправлять MT SM, получать отчёты о доставке и MO SM. При этом по каким-то причинам нам нужны кастомные PDU и полный контроль над линком, поэтому за формирование submit_sm PDU и обработку всех входящих PDU мы будем отвечать сами. При этом мы не должны забывать про требование асинхронности.


Работа с линком


Надеюсь, что сложностей с установкой зависимости из hex.pm у вас не возникло и мы можем приступить к написанию кода. Как говорилось выше, работать мы будем в асинхронном режиме, поэтому запускаем клиента с помощью модуля SMPPEX.ESME:


'Elixir.SMPPEX.ESME':start_link(SmscHost, SmscPort, {?MODULE, [Opts]})

Для синхронного режима существует SMPPEX.ESME.Sync.


Наш клиент готов, и мы можем сделать привязку к SMSC. Предположим, что SMSC поддерживает SMPPv3.4 и мы можем использовать transceiver режим:


'Elixir.SMPPEX.Pdu.Factory':bind_transceiver(SystemId, Pass)

Если всё прошло хорошо, нам должен прийти PDU с командой bind_transceiver_resp:


bind_transceiver_resp = 'Elixir.SMPPEX.Pdu':command_name(Pdu)

Формирование PDU для MT SM


Линк поднят, и мы можем отправить наше сообщение. Соберём PDU для него:


submit_sm_pdu(SourceMsisdn, DestMsisdn, Message, Ttl) ->  {ok, CommandId} = 'Elixir.SMPPEX.Protocol.CommandNames':id_by_name(submit_sm),  {D, {H, M, S}} = calendar:seconds_to_daystime(Ttl),  VP = lists:flatten(io_lib:format("0000~2..0w~2..0w~2..0w~2..0w000R", [D, H, M, S])),  'Elixir.SMPPEX.Pdu':new(    CommandId,    #{      source_addr => SourceMsisdn,      source_addr_ton => 1,      source_addr_npi => 1,      destination_addr => DestMsisdn,      dest_addr_ton => 1,      dest_addr_npi => 1,      short_message => Message,      data_coding => 246,      protocol_id => 127,      %% For concatenated messages      esm_class => 64,      registered_delivery => 1,      validity_period => list_to_binary(VP)    }  ).

Обработка отчетов о доставке и MO SM


После отправки сообщения в линк SMSC ответит нам submit_sm_resp, в котором указан уникальный ID нашего сообщения:


MsgId = 'Elixir.SMPPEX.Pdu':mandatory_field(Pdu, message_id)

Теперь нам необходимо дождаться deliver_sm с этим message_id.


Чтобы отличить отчёты о доставке от MO SM, проанализируем esm_class:


EsmClass = 'Elixir.SMPPEX.Pdu':mandatory_field(Pdu, esm_class),case <<EsmClass>> of  <<_Head : 2, 0 : 1, 0 : 1, 0 : 1, 1 : 1, _Tail : 2>> -> handle_delivery_receipt(Pdu);  <<_Head : 2, 0 : 1, 0 : 1, 0 : 1, 0 : 1, _Tail : 2>> -> handle_standart_message(Pdu);  Some -> ?LOG_ERROR("unknown deliver_sm: ~p", [Some])end

При этом для обработки отчётов о доставке нам достаточно узнать ID доставленного сообщения:


SmsId = 'Elixir.SMPPEX.Pdu':field(Pdu, receipted_message_id)

А для входящих сообщений узнать номер отправителя:


Msisdn = 'Elixir.SMPPEX.Pdu':field(Pdu, source_addr)

и полезное содержимое сообщения:


Payload = 'Elixir.SMPPEX.Pdu':field(Pdu, short_message)

Как известно, спецификация SMPP требует deliver_sm_resp в ответ на deliver_sm. Поэтому после обработки отчёта о доставке и входящего сообщения мы должны ответить deliver_sm_resp. Создадим PDU для него:


deliver_sm_resp_pdu(MessageId) ->  {ok, CommandId} = 'Elixir.SMPPEX.Protocol.CommandNames':id_by_name(deliver_sm_resp),  CommandStatus = 0,  SeqNumber = 0,  'Elixir.SMPPEX.Pdu':new({CommandId, CommandStatus, SeqNumber}, #{message_id => MessageId}, #{}).

Я специально не указываю номер команды, добавим его автоматически:


ReplyPdu = 'Elixir.SMPPEX.Pdu':as_reply_to(deliver_sm_resp(SmsId), Pdu)

Весь код демопроекта можно найти в репозитории: https://github.com/Elzor/smpp_in_erlang.


OTP-тренды


В 2020 году на тренды развития OTP и BEAM всё большее влияние оказывает сообщество Elixir. Чаще и чаще хорошие инструменты и полезные библиотеки можно найти на Elixir, а не на Erlang. Это не повод для тревоги за Erlang, просто Elixir смог заинтересовать и привлечь больше людей в своё сообщество, и это прекрасно. А благодаря OTP для использования той или иной библиотеки нам не важно, на чём она написана. Надеюсь, пример из статьи смог показать гибкость SMPPEX как инструмента и удобство применения библиотек, написанных на Elixir в Erlang-проектах.

Подробнее..

To spawn, or not to spawn?

11.04.2021 18:19:06 | Автор: admin

Вот в чём вопрос! Что лучше - держать всё в одном процессе, или создавать отдельный процесс на каждый кусок состояния, которым нам нужно управлять? В этой статье я немного расскажу об использовании или неиспользовании процессов. Я также расскажу, как отделить сложную логику с отслеживанием состояния от таких проблем, как временное (темпоральное) поведение и межпроцессное взаимодействие.

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

  • Используйте функции и модули для разделения мыслительных сущностей.

  • Используйте процессы для разделения сущностей времени выполнения.

  • Не используйте процессы (даже агентов) для разделения сущностей мышления.

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

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

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

Пример

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

Раунд - это, по сути, последовательность раздач, каждая из которых принадлежит отдельному игроку. Раунд начинается с первой руки. Игроку сначала выдают две карты, а затем он делает ход: берет еще одну карту (хит) или останавливается (стоп). В первом случае игроку дается еще одна карта. Если счет игрока больше 21, игрок вылетает из игры. В противном случае игрок может сделать еще один ход (взять карту или остановиться).

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

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

Для простоты я не рассматривал такие понятия, как дилер, ставки, страхование, разделение(), несколько раундов, люди, присоединяющиеся к столу или покидающие его.

Границы процесса

Итак, нам нужно отслеживать различные типы состояний, которые меняются с течением времени: колода карт, руки каждого игрока и состояние раунда. Наивный подход к этому - использование нескольких процессов. У нас может быть один процесс для каждой руки, другой процесс для колоды карт и главный процесс, который управляет всем раундом. Я вижу, что люди иногда используют аналогичный подход, но я совершенно не уверен, что это правильный путь. Основная причина в том, что игра по своей природе очень синхронизирована. Все происходит одно за другим в четко определенном порядке: я получаю свои карты, делаю один или несколько ходов, и когда я закончу, вы будете следующим. В любой момент времени в одном раунде происходит только одно действие.

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

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

В своих выступлениях я часто говорю, что в сложных системах существует огромный потенциал параллелизма, поэтому мы будем использовать множество процессов. Но чтобы воспользоваться этими преимуществами, нам нужно использовать процессы там, где они имеют смысл.

Итак, учитывая все обстоятельства, я почти уверен, что единственный процесс для управления всем состоянием одного раунда - это правильный путь. Было бы интересно посмотреть, что изменится, если мы введем концепцию стола, где раунды играются постоянно, а игроки меняются со временем. Я не могу сказать наверняка на данный момент, но я думаю, что это интересное упражнение на случай, если вы захотите его изучить :-)

Функциональное моделирование

Итак, как же мы можем разделить разные сущности без использования нескольких процессов? Конечно, используя функции и модули. Если мы распределим разные части логики по разным функциям, дадим этим функциям собственные имена и, возможно, организуем их в правильно названные модули, мы сможем прекрасно представить наши идеи без необходимости имитировать объекты с помощью агентов.

Позвольте мне показать вам, что я имею в виду, проведя вас через каждую часть моего решения, начиная с самого простого.

Колода карт

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

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

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

@cards (  for suit <- [:spades, :hearts, :diamonds, :clubs],      rank <- [2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king, :ace],    do: %{suit: suit, rank: rank})

Теперь я могу добавить функцию shuffle/0 для создания перемешанной колоды:

def shuffled(), do:  Enum.shuffle(@cards)

И наконец, take/1, которая берёт верхнюю карту из колоды:

def take([card | rest]), do:  {:ok, card, rest}def take([]), do:  {:error, :empty}

Функция take/1 возвращает либо {:ok, card_taken, rest_of_the_deck}, либо {:error, :empty}. Такой интерфейс заставляет клиента (пользователя абстракции колоды) явно решать, как поступать в каждом случае.

Как мы можем это использовать:

deck = Blackjack.Deck.shuffled()case Blackjack.Deck.take(deck) do  {:ok, card, transformed_deck} ->    # do something with the card and the transform deck  {:error, :empty} ->    # deck is empty -> do something elseend

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

  • кучи связанных функций,

  • с описательными именами,

  • которые не проявляют побочных эффектов,

  • и могут быть извлечены в отдельный модуль

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

Не так важно, находятся ли эти функции в выделенном модуле. Код этой абстракции довольно прост и используется только в одном месте. Поэтому я мог бы также определить приватные функции shuffled_deck/0 и take_card/1 в клиентском модуле. Фактически, это то, что я часто делаю, если код достаточно мал. Я всегда могу выделить это позже, если что-то усложнится. (прим. переводчика: не совсем уловил здесь мысль, которую хотел донести автор)

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

Полный код модуля доступен здесь.

Рука

Эту же технику можно использовать для управления рукой. Эта абстракция отслеживает карты в руке. Она также умеет подсчитывать очки и определять статус руки (:ok или :busted). Реализация находится в модуле Blackjack.Hand.

Модуль выполняет две функции. Мы используем new/0 для создания экземпляра руки, а затем deal/2, чтобы раздать карту руке. Вот пример комбинации руки и колоды:

# create a deckdeck = Blackjack.Deck.shuffled()# create a handhand = Blackjack.Hand.new()# draw one card from the deck{:ok, card, deck} = Blackjack.Deck.take(deck)# give the card to the handresult = Blackjack.Hand.deal(hand, card)

Результат deal/2 вернётся в форме {hand_status, transformed_hand}, где hand_status это или :ok или :busted.

Раунд

Эта абстракция, реализованная в модуле Blackjack.Round, связывает всё воедино. Она имеет следующие обязанности:

  • сохранять состояния колоды

  • держать состояние всех рук в раунде

  • решать, кому переходит следующий ход

  • получать и интерпретировать ход игрока (хит / стоп)

  • брать карты из колоды и передавать их текущей руке

  • вычислять победителя после того, как все руки разыграны

Абстракция раунда будет следовать тому же функциональному подходу, что и колода с рукой. Однако здесь есть дополнительный поворот, который касается выделения темпоральной логики. Раунд занимает некоторое время и требует взаимодействия с игроками. Например, когда начинается раунд, первый игрок должен быть проинформирован о первых двух картах, которые он получил, а затем он должен быть проинформирован о том, что пришла его очередь сделать ход. Затем раунду нужно дождаться, пока игрок сделает ход, и только после этого он сможет пойти дальше.

У меня сложилось впечатление, что многие люди, включая опытных эрлангистов/эликсирщиков, реализовали бы концепцию раунда непосредственно в GenServer или в :gen_statem. Это позволит им управлять состоянием раунда и темпоральной логикой (например, общением с игроками) в одном месте.

Однако я считаю, что эти два аспекта необходимо разделить, поскольку оба они потенциально сложны. Логика одного раунда уже в некоторой степени запутана, и она может только ухудшиться, если мы захотим поддержать дополнительные аспекты игры, такие как ставки, сплиты или раздающего игрока. Общение с игроками имеет свои проблемы, если мы хотим иметь дело с расщеплениями сети(netsplits), сбоями, медленными или недоступными клиентами. В этих случаях нам может потребоваться поддержка повторных попыток, возможно, добавить некоторую персистентность, event sourcing или что-то еще.

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

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

Позвольте показать вам код. Чтобы создать новый раунд, мне нужно вызвать start/1:

{instructions, round} = Blackjack.Round.start([:player_1, :player_2])

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

  • создание руки для каждого игрока

  • отслеживание текущего игрока

  • отправка уведомлений игрокам

    Функция возвращает кортеж. Первый элемент кортежа - это список инструкций. Пример:

    [{:notify_player, :player_1, {:deal_card, %{rank: 4, suit: :hearts}}},{:notify_player, :player_1, {:deal_card, %{rank: 8, suit: :diamonds}}},{:notify_player, :player_1, :move}]
    

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

  • уведомить игрока 1, что он получил четвёрку червей

  • уведомить игрока 1, что он получил восьмёрку бубён

  • уведомить игрока 1, что ему нужно сделать ход

    Фактическая доставка этих уведомлений заинтересованным игрокам является ответственностью клиентского кода. Клиентским кодом может быть, скажем, GenServer, который будет отправлять сообщения процессам игроков. Он также будет ждать, пока игроки не сообщат, когда они захотят взаимодействовать с игрой. Это временная(темпоральная) логика, и она полностью хранится за пределами модуля Round.

Второй элемент возвращённого кортежа, называется round, это состояние самого раунда. Стоит отметить, что эти данные типизированы как непрозрачные. Это значит, что клиент не может читать эти данные внутри переменной round. Всё, что нужно клиенту, будет доставлено в списке instruction.

Давайте продвинемся на шаг вперед в этом раунде, взяв следующую карту игроком 1:

{instructions, round} = Blackjack.Round.move(round, :player_1, :hit)

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

Вот инструкции, которые я получил:

[  {:notify_player, :player_1, {:deal_card, %{rank: 10, suit: :spades}}},  {:notify_player, :player_1, :busted},  {:notify_player, :player_2, {:deal_card, %{rank: :ace, suit: :spades}}},  {:notify_player, :player_2, {:deal_card, %{rank: :jack, suit: :spades}}},  {:notify_player, :player_2, :move}]

Этот список говорит мне, что игрок 1 получил десятку пик. Поскольку раньше у него было 4 червы и 8 бубён, игрок вылетает из игры, и раунд немедленно переходит к следующей руке. Клиенту предлагается уведомить игрока 2 о том, что у него есть две карты, и что он должен сделать ход.

Сделаем ход от имени игрока 2:

{instructions, round} = Blackjack.Round.move(round, :player_2, :stand)# instructions:[  {:notify_player, :player_1, {:winners, [:player_2]}}  {:notify_player, :player_2, {:winners, [:player_2]}}]

Игрок 2 не взял другую карту, поэтому его рука завершена. Абстракция немедленно определяет победителя и инструктирует нас проинформировать обоих игроков о результате.

Давайте посмотрим, как Round прекрасно сочетается с абстракциями Deck и Hand. Следующая функция из модуля Round берет карту из колоды и передает ее текущей руке:

defp deal(round) do  {:ok, card, deck} =    with {:error, :empty} <- Blackjack.Deck.take(round.deck), do:      Blackjack.Deck.take(Blackjack.Deck.shuffled())  {hand_status, hand} = Hand.deal(round.current_hand, card)  round =    %Round{round | deck: deck, current_hand: hand}    |> notify_player(round.current_player_id, {:deal_card, card})  {hand_status, round}end

Берём карту из колоды, используя новую колоду, если текущая закончилась. Затем мы передаем карту в текущую руку, обновляем раунд новой рукой и статусом колоды, добавляем инструкцию по уведомлению о данной карте и возвращаем статус руки (:ok или :busted) и обновленный раунд. Никаких дополнительных процессов в этом процессе не задействовано :-)

Вызов notify_player - это простой однострочник, который избавляет этот модуль от многих сложностей. Без него нам нужно было бы отправить сообщение другому процессу (например, другому GenServer или каналу Phoenix). Пришлось бы как-то найти этот процесс и рассмотреть случаи, когда этот процесс не запущен. Вместе с кодом, который моделирует ход раунда, пришлось бы связать много дополнительных сложностей.

Но благодаря механизму инструкций, ничего из этого не случилось, и модуль Round остался сфокусированным на правилах игры. Функция notify_player будет сохранять инструкцию. Позже, перед выходом, функция take_instructions из Round будет забирать все ожидающие инструкции, и возвращать их по отдельности, вынуждая клиентский код интерпретировать их.

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

Организация процесса

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

Сервер раунда

Каждый раунд управляется модулем Blackjack.RoundServer, который есть GenServer. Agent также мог бы подойти для этих целей, но я не фанат агентов, так что я остановлюсь на GenServer. Ваши предпочтения могут отличаться, конечно, и я полностью уважаю ваше мнение :-)

Чтобы запустить процесс, нам нужно вызвать функцию start_playing/2. Это имя выбрано вместо более распространенного start_link, поскольку start_link по соглашению ссылается на вызывающий процесс. Напротив, start_playing начнет раунд где-то еще в дереве надзора, и процесс не будет связан с вызывающим.

Функция принимает два аргумента: идентификатор раунда и список игроков. Идентификатор раунда - это произвольный уникальный терм, который должен быть выбран клиентом. Серверный процесс будет зарегистрирован во внутреннем реестре с использованием этого идентификатора.

Каждая запись в списке игроков представляет собой словарь, описывающий клиентскую часть игрока:

@type player :: %{id: Round.player_id, callback_mod: module, callback_arg: any}

Игрок описывается его идентификатором, модулем обратного вызова и аргументом обратного вызова. Идентификатор будет передан абстракции раунда. Всякий раз, когда абстракция инструктирует сервер уведомить некоторого игрока, сервер вызывает callback_mod.some_function (some_arguments), где some_arguments будет включать идентификатор раунда, идентификатор игрока, callback_arg и дополнительные аргументы, специфичные для уведомления.

Подход callback_mod позволяет нам поддерживать различные типы игроков, такие как:

  • игроков, подключенных через HTTP

  • игроков, подключенных через настраиваемый протокол TCP

  • игрок в сеансе оболочки iex

  • автоматических игроков (ботов)

    Мы легко справимся со всеми этими игроками в одном раунде. Сервер не заботится ни о чём из этого, он просто вызывает функции обратного вызова модуля обратного вызова и позволяет реализации выполнять работу.

Функции, которые должны быть реализованы в модуле обратного вызова, перечислены здесь:

@callback deal_card(RoundServer.callback_arg, Round.player_id,  Blackjack.Deck.card) :: any@callback move(RoundServer.callback_arg, Round.player_id) :: any@callback busted(RoundServer.callback_arg, Round.player_id) :: any@callback winners(RoundServer.callback_arg, Round.player_id, [Round.player_id])  :: any@callback unauthorized_move(RoundServer.callback_arg, Round.player_id) :: any

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

Другое приятное следствие такого дизайна - это то, что тестирование этого сервера довольно просто. Тест реализует уведомления путём отправки сообщений самому себе из каждого колбека. Затем тестирование сводится к asserting/refuting определённых сообщений, и вызову RoundServer.move/3, чтобы сделать ход от имени игрока.

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

Когда функция модуля Round возвращает список инструкций серверному процессу, тот пройдёт по этому списку, и интерпретирует инструкции.

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

Это реализовано в модуле Blackjack.PlayerNotifier, процессе на основе GenServer, чья роль - отправлять уведомление отдельному игроку. Когда мы стартуем сервер раунда функцией start_playing/2, запускается небольшое поддерево надзора в котором размещается сервер раунда вместе с одним сервером уведомлений на каждого игрока в раунде.

Когда сервер раунда делает ход, он получает список инструкций от абстракции раунда. Затем он перенаправляет каждую инструкцию соответствующему серверу уведомлений, который интерпретирует эту инструкцию и вызывает соответствующий модуль/функцию/аргументы(M/F/A) для уведомления игрока.

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

[  {:notify_player, :player_1, {:deal_card, %{rank: 10, suit: :spades}}},  {:notify_player, :player_1, :busted},  {:notify_player, :player_2, {:deal_card, %{rank: :ace, suit: :spades}}},  {:notify_player, :player_2, {:deal_card, %{rank: :jack, suit: :spades}}},  {:notify_player, :player_2, :move}]

Может случиться так, что сообщения player_2 придут до того, как player_1 будет проинформирован о том, что он остановлен. Но это нормально, ведь это два разных игрока. Порядок сообщений для каждого игрока, конечно же, сохраняется, благодаря процессу сервера уведомлений, зависящему от конкретного игрока.

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

Сервис блэкджека

Картинка завершается в виде приложения OTP :blackjack (модуль Blackjack). Когда вы запускаете приложение, запускается пара локально зарегистрированных процессов: экземпляр внутреннего реестра Registry (используется для регистрации серверов раунда и уведомлений) и супервизор :simple_one_for_one, который будет размещать поддерево процесса для каждого раунда.

Это приложение теперь в основном представляет собой сервис блэкджека, который может управлять несколькими раундами. Сервис является универсальным и не зависит от конкретного интерфейса. Вы можете использовать его с Phoenix, Cowboy, Ranch (для простого TCP), elli или любым другим, подходящим для ваших целей. Вы реализуете модуль обратного вызова, запускаете клиентские процессы и запускаете сервер раунда.

Вы можете посмотреть примеры в модуле Demo, который реализует простого автоигрока, модуль обратного вызова сервиса уведомлений, основанного на GenServer, и логику старта, которая стартует раунд с пятью игроками:

$ iex -S mixiex(1)> Demo.runplayer_1: 4 of spadesplayer_1: 3 of heartsplayer_1: thinking ...player_1: hitplayer_1: 8 of spadesplayer_1: thinking ...player_1: standplayer_2: 10 of diamondsplayer_2: 3 of spadesplayer_2: thinking ...player_2: hitplayer_2: 3 of diamondsplayer_2: thinking ...player_2: hitplayer_2: king of spadesplayer_2: busted...

Вот как выглядит дерево надзора, когда у нас есть пять одновременных раундов, в каждом по пять игроков:

Заключение

Итак, можем ли мы управлять сложным состоянием в одном процессе? Конечно, можем! Простые функциональные абстракции, такие как Deck and Hand, позволили мне разделить проблемы более сложного состояния раунда без необходимости прибегать к помощи агентов.

Однако это не означает, что мы должны быть консервативными в отношении процессов. Используйте процессы везде, где они имеют смысл и приносят очевидные преимущества. Выполнение различных раундов в отдельных процессах улучшает масштабируемость, отказоустойчивость и общую производительность системы. То же самое касается процессов уведомления. Это разные задачи среды выполнения, поэтому нет необходимости запускать их в одном контексте времени выполнения.

Если временная логика и/или логика предметной области сложны, рассмотрите возможность их разделения. Подход, который я использовал, позволил мне реализовать более сложное поведение во время выполнения (одновременные уведомления), не усложняя бизнес-процесс раунда. Это разделение также ставит меня в удобное положение, поскольку теперь я могу развивать оба аспекта по отдельности. Добавление поддержки бизнес-концепций дилера, сплита, страхования и других не должно существенно влиять на аспект выполнения. Точно так же поддержка расщеплений сети(netsplits), повторных подключений, сбоев игрока или тайм-аутов не должна требовать изменений в логике домена.

Наконец, стоит помнить о конечной цели. Хотя я туда не ходил (пока), я всегда планировал, что этот код будет размещен на каком-то веб-сервере. Так что некоторые решения принимаются в поддержку этого сценария. В частности, реализация RoundServer, которая принимает модуль обратного вызова для каждого игрока, позволяет мне подключаться к различным типам клиентов, использующим различные технологии. Это делает сервис блэкджека независимым от конкретных библиотек и фреймворков (за исключением стандартных библиотек и OTP, конечно) и делает его полностью гибким.

Подробнее..

Из песочницы Создаем конечный автомат в Elixir и Ecto

20.07.2020 18:22:00 | Автор: admin
Существует много полезных шаблонов проектирования и концепция конечного автомата входит в число полезных шаблонов проектирования.

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

В этой публикации вы узнаете, как реализовать этот шаблон с помощью Elixir и Ecto.

Случаи использования


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

Примеры:

  • Регистрация в личном кабинете. В этом процессе пользователь сначала регистрируется, потом добавляет некоторую дополнительную информацию, затем подтверждает свою электронную почту, затем включает 2FA, и только после этого получает доступ в систему.
  • Корзина для покупок. Сперва она пустая, потом в неё можно добавить товары и после чего пользователь может перейти к оплате и доставке.
  • Конвейер задач в системах управления проектами. Например: изначально задачи в статусе "создана", потом задача может быть "назначена" исполнителю, затем статус изменится на "в процессе", а затем в "выполнено".

Пример конечного автомата


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

Дверь может быть заблокирована или разблокирована. Она также может быть открыта или закрыта. Если она разблокирована, то её можно открыть.

Мы можем смоделировать это как конечный автомат:

image

Этот конечный автомат имеет:

  • 3 возможных состояния: заблокирована, разблокирована, открыта
  • 4 возможных перехода состояния: разблокировать, открыть, закрыть, заблокировать

Из диаграммы можно сделать вывод, что невозможно перейти от заблокирована к открыта. Или простыми словами: сначала нужно разблокировать дверь, а уже потом открыть. Данная диаграмма описывает поведение, но как реализовать это?

Конечные автоматы как Elixir процессы


Начиная с OTP 19, Erlang предоставляет модуль :gen_statem, который позволяет реализовывать процессы, подобные gen_server, которые ведут себя как конечные автоматы (в которых текущее состояние влияет на их внутреннее поведение). Давайте посмотрим, как это будет выглядеть для нашего примера с дверью:

defmodule Door do  @behaviour :gen_statem # Стартуем сервис def start_link do   :gen_statem.start_link(__MODULE__, :ok, []) end  # начальное состояние, вызываемое при старте, locked - заблокировано @impl :gen_statem def init(_), do: {:ok, :locked, nil}  @impl :gen_statem def callback_mode, do: :handle_event_function  # обработка приходящего события: разблокируем заблокированную дверь # next_state - новое состояние - дверь разблокирована @impl :gen_statem def handle_event({:call, from}, :unlock, :locked, data) do   {:next_state, :unlocked, data, [{:reply, from, {:ok, :unlocked}}]} end  # блокировка разблокированной двери def handle_event({:call, from}, :lock, :unlocked, data) do   {:next_state, :locked, data, [{:reply, from, {:ok, :locked}}]} end  # открытие разблокированной двери def handle_event({:call, from}, :open, :unlocked, data) do   {:next_state, :opened, data, [{:reply, from, {:ok, :opened}}]} end  # закрытие открытой двери def handle_event({:call, from}, :close, :opened, data) do   {:next_state, :unlocked, data, [{:reply, from, {:ok, :unlocked}}]} end  # возвращение ошибки при неопределеном поведении def handle_event({:call, from}, _event, _content, data) do   {:keep_state, data, [{:reply, from, {:error, "invalid transition"}}]} endend

Этот процесс начинается в состоянии :locked (заблокировано). Отправляя соответствующие события, мы можем сопоставить текущее состояние с запрошенным переходом и выполнить необходимые преобразования. Дополнительный аргумент data сохраняется для любого другого дополнительного состояния, но в этом примере мы его не используем.

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

{:ok, pid} = Door.start_link():gen_statem.call(pid, :unlock)# {:ok, :unlocked}:gen_statem.call(pid, :open)# {:ok, :opened}:gen_statem.call(pid, :close)# {:ok, :closed}:gen_statem.call(pid, :lock)# {:ok, :locked}:gen_statem.call(pid, :open)# {:error, "invalid transition"}

Если наш конечный автомат в большей степени ориентирован на данные, чем на процессы, то мы можем использовать другой подход.

Конечные автоматы как Ecto модели


Есть несколько пакетов Elixir, которые решают эту проблему. В этом посте я буду использовать Fsmx, но другие пакеты, например, Machinery, также предоставляют аналогичные функции.

Этот пакет позволяет нам моделировать точно такие же состояния и переходы, но в существующей модели Ecto:

defmodule PersistedDoor do use Ecto.Schema  schema "doors" do   field(:state, :string, default: "locked")   field(:terms_and_conditions, :boolean) end  use Fsmx.Struct,   transitions: %{     "locked" => "unlocked",     "unlocked" => ["locked", "opened"],     "opened" => "unlocked"   }end

Как мы увидеть, Fsmx.Struct получает все возможные переходы в качестве аргумента. Это позволяет ему проверять нежелательные переходы и предотвращать их возникновение. Теперь мы можем изменить состояние, используя традиционный, не-Ecto подход:

door = %PersistedDoor{state: "locked"} Fsmx.transition(door, "unlocked")# {:ok, %PersistedDoor{state: "unlocked", color: nil}}

Но мы можем также попросить то же самое в форме Ecto changeset (используемое в Elixir слово, означающее набор изменений):

door = PersistedDoor |> Repo.one()Fsmx.transition_changeset(door, "unlocked")|> Repo.update()

Этот changeset только обновляет поле :state. Но мы можем расширить его, чтобы включить дополнительные поля и проверки. Допустим, чтобы открыть дверь, нам нужно принять ее условия:

defmodule PersistedDoor do # ...  def transition(changeset, _from, "opened", params) do   changeset   |> cast(params, [:terms_and_conditions])   |> validate_acceptance(:terms_and_conditions) endend

Fsmx ищет в вашей схеме необязательную функцию transition_changeset/4 и вызывает ее как с предыдущим состоянием, так и со следующим. Вы можете сопоставить их по шаблону, чтобы добавить определенные условия для каждого переход.

Работа с побочными эффектами


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

Ecto работает с атомарностью через пакет Ecto.Multi, которая группирует несколько операций внутри транзакции базы данных. Ecto также имеет функцию Ecto.Multi.run/3, которая позволяет запускать произвольный код в рамках той же транзакции.

Fsmx, в свою очередь, интегрируется с Ecto.Multi, предоставляя вам возможность выполнять переходы состояний как часть Ecto.Multi, а также предоставляет дополнительный обратный вызов, который выполняется в этом случае:

defmodule PersistedDoor do # ...  def after_transaction_multi(changeset, _from, "unlocked", params) do   Emails.door_unlocked()   |> Mailer.deliver_later() endend

Теперь вы можете выполнить переход как показано:

door = PersistedDoor |> Repo.one() Ecto.Multi.new()|> Fsmx.transition_multi(schema, "transition-id", "unlocked")|> Repo.transaction()

Эта транзакция будет использовать тот же transition_changeset/4, как было описано выше, для вычисления необходимых изменений в Ecto модели. И будет включать новый обратный вызов в качестве вызова Ecto.Multi.run. В результате электронное письмо отправляется (асинхронно, с использованием Bamboo, чтобы не запускаться внутри самой транзакции).

Если changeset (набор изменений) по какой-либо причине признан недействительным, электронное письмо никогда не будет отправлено, в результате атомарного выполнения обеих операций.

Заключение


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

Сделаю оговорку, возможно акторная модель способствует простоте реализации конечного автомата в Elixir\Erlang, каждый актор имеет своё состояние и очередь входящих сообщений, которые последовательно изменяют его состояние. В книге Проектирование масштабируемых систем в Erlang/ОТР про конечные автоматы очень хорошо написано, в разрезе акторной модели.

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru