Предыстория

Закончилась ещё одна итерация связанная с "совершенствованием" (или деградацией) бота aleesa.

Когда-то давно, году эдак в 2008-м этот бот начинался, как забавный развлекательный сервис в конференции slackware на jabber.ru. Время шло, бОльшая часть участников того канальчика разошлись своими путями... С кем-то мы встретились снова, а кто-то пропал из поля зрения. Сама конференция перешла в telegram, но комната на jabber.ru осталась. И внезапно она не пустая.

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

Начальная инкарнация бота - это альфа-версия бота sulci. Автор бота работала в компании Яндекс и бот был скорее pet-проектом для расширения кругозора, чем чем-то серьёзным. Насколько мне известно, в какой-то момент времени она переехала в Испанию и бот оказался заброшеным. А поскольку он написан на ЯП ocaml, а это довольно экзотический язык, то поддерживать его оказалось некому. Sulci был интересен тем, что в нём был встроенный бредогенератор на основе цепей Маркова. И работал он странно, но забавно и вокруг этой фишки был построен весь бот. Остальное - это приятный бонус.

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

Однако в ходе эксплуатации выяснилось, что xmpp-либа, которая есть в перле, обладает фатальным недостатком - она абсолютно не умеет обнаруживать, что соединение с сервером пропало. Я уж не говорю о том, что она абсолютно не умеет в обнаружение того, что бот больше не в комнате... Это довольно серьёзные недостатки, так как рано или поздно бот от конфы "отваливался". И есть ещё один существенный недостаток - бот использует собственную реализацию event loop-а - это не mojo, не anyevent и не poe. То есть интеграция его с этими event loop-ами будет болезненной - в приложении не может быть 2-х разных event loop-ов.

Собственно, история

Кроме xmpp-бота у меня есть бот для telegram-а. И с некоторого времени он уже модульный: часть модулей на гошке, а часть - на перле. И благодаря модульности, у меня есть ещё и фронт-энд для irc, который тоже умеет в бОльшую часть сервисов, в которые умеет телеграммный бот. В том числе и в бредогенератор.

И возникла мысль - пора перенести xmpp-бота на этот модульный движок.

Переносить на перле - "ну такое". Придётся переписать собственно весь движок с кастомного event loop-а на что-то более "стандартное". На тот же anyevent, например. И... это в какой-то мере боль. Да, при этом я не связан узами обратной совместимости, но... кидаться xml-ками по tcp-соединению - развлечение "ниже среднего". Тем более, что у меня уже есть небольшой опыт программирования на гошке. Почему бы не написать этого бота на гошке?

Итак, гошка, либа go-xmpp от товарища mattn-а с гитхаба. Первая попавшаяся из относительно "свежих". Она же относительно минималистичная.

Судя по всему, мне везёт на забрасываемые либы - в ходе своих программизмов, мне понадобилось расширить и углубить возможности либы, однако, автор PR-ы пока не принял, а прошло уже 3+ недели... Поэтому я просто форкнул его либу себе. Хотя я такое не оч люблю.

В процессе генерации клиента xmpp, мне пришлось почитать rfc и разные xep-ы, поскольку либа минималистичная, то "саночки" для бизнес-логики приходится писать самостоятельно. Вплоть до обработки ошибок и реакции на служебные запросы. В этой либе даже нету event loop-а. Но это не проблема. Я могу написать свою пародию на ивент луп.

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

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

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

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

Довольно охренительных историй, где же рант, ради которого мы тут собрались?

Да собственно, вот он.

Итак, начнём с начала. У протокола xmpp, как такового, "цельного" стандарта нету. Он разбит по фичам, так называемым xep-ам. Например, xep-0199 описывает как работает ping. Client-to-server, server-to-client, client-to-client, server-to-server, component-to-client. Xep-0410 описывает, как работает ping muc-ов. Или xep-0030 описывает механизм service discovery. Итд. У каждого xep-а есть степень готовности. Или степень "стандартности" - предложение, черновик, утверждённый стандарт, отозванный xep, устаревший стандарт. И заминка в том, что некоторые важные (казалось бы) стандарты находятся в стадии черновиков. Классический пример - service discovery, но о нём позже.

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

Более того, MUC-и расположены на отдельном домене. То есть если сервис - это jabber.ru, то сервис с muc-ами - это conference.jabber.ru и поддомен conference является "традиционным" для сервиса с MUC-ами.

Эта особенность является одной из важных - в единой конфедерации jabber-серверов, можно авторизоваться на одном сервере, и прозрачно общаться в комнатах с muc-а другого сервера, дополнительно нигде не авторизуясь. Например, я авторизуясь на jabber.ru могу общаться в комнатах @conference.jabber.ru, @conference.xmpp.ru, @conference.jabbim.cz, @conference.xmpp.jp итд.

И тут возникает другой момент - всё моё общенние в этих комнатах происходит через сервер jabber.ru, а с целевыми доменами (conference.чего-то.там) уже jabber.ru устанавливает соединение и общается по протоколу xmpp s2s.

Если та сторона проводит некие технические работы и рестартует сервис, то я, как клиент, об этом не узнаю, потому что хоть s2s и потеряет "моё" соединение, но меня об этом никак не уведомит, потому что этот момент не описан в rfc вообще никак и никаких xep-ов на эту тему нету.

Чтобы определять проблемы со связностью muc-ов, в xmpp предусмотрен костыль - пинг. Например, пинг со своего jid-а на jabber.ru своего же jid-а, но уже, например, на conference.xmpp.jp.

И это вместо того, чтобы прислать мне iq stanza о том, что service-unavailable. Не хочешь IQ error? Не вопрос - можно прислать presence type unavalable с моим "комнатным" jid-ом в качестве отправителя. Но нет. Об этом как-то "не подумали". Решили, что костыли в виде пингов - это збс.

Ну, пинг и пинг, что в этом такого? спросите вы. А я отвечу, что механизмов у xmpp для совершения таких пингов целых два. Всратый "классический" и более вменяемый server-optimized.

И... вот тут имеет смысл вернуться к service discovery - возможность сервиса работать с server-optimized-muc-ping вытаскивается из списка features из ответа на запрос service discovery к MUC-у. Но фикус в том, что service discovery - это опциональная фишка, потому что стандарт находится в состоянии draft, то есть черновика. И теоретически возможна ситуация, когда дискавера нету, но сервер умеет в server-optimized-muc-ping.

Впрочем, справедливости ради, я пока не нарывался на то, что service discovery сервером не поддерживается. Другое дело, что я нарывался на то, что дискавер на ejabberd не совсем строго проверяет наличие тех или иных лишних полей в xml-ке от клиента... но это уже другой вопрос. Валидный discover-запрос ejabberd парсит нормально.

Так вот, перед тем как делать пинг самого себя в MUC-е, неплохо бы продискаверить, что поддерживает muc и чего не поддерживает - как раз с помощью опционального service discovery. От этого зависит какой ответ от сервера мы будем ждать.

И тут надо сделать небольше лирическое отступление. В связи с тем, что MUC-и у нас отдельные, jid-ы там тоже отдельные. Если обычный jid у меня например eleksir@jabber.ru/ресурс, то в конференции у меня будет другой jid - myroom@conference.jabber.ru/eleksir8. В чём прикол? А их тут не один - в конференции та часть, которая меня идентифицирует (которая тут значится как eleksir8) может быть любой, джаббер клиент вообще спрашивает, что ты хочешь использовать в качестве ника в конференции. Мало того, если админам конфы оригинальный jid виден всегда, то всех остальных можно ограничить, чтобы оригинальный jid они не видели.

Итак вернёмся к нашим баранам - всратый пинг MUC-а. Или тот пинг, которым оперирует ejebberd и конкретно jabber.ru. Я от своего jid-а направляю IQ get запрос своему же jid-у, но в коференции. Мне приходит "отражённый" запрос от меня же - IQ get - на который я отвечаю IQ result и мне приходит iq result от моего ника в комнате. В чём подвох? а подвох в том, что если у меня не один клиент, то сервер может "отражённый" IQ get отправить... моему другому клиенту. А другой клиент, например, может уйти поспать или быть на мобильном телефоне, который потерял сотовую связь по каким-то капризам эфира и... он может не ответить на этот IQ get. То есть пинг потеряется. А мы же пингуем не для собственного удовольствия и не для развлечения, а с целью определить, не отвалился ли наш клиент от комнаты. И получается, что если у одного из клиентов какие-то проблемы, то страдают все подключённые клиенты, а такого быть не должно.

И server optimized ping. Или пинг здорового человека. Которым не оперирует ejebberd и jabber.ru. Механика простая: я отправляю IQ get запрос со своего локального jid-а на свой же jid "в комнате" и мне приходит iq result с сервера, как будто я пропинговал кого-то ещё. Безо всяких "отражённых" IQ get-ов.

А ложка дёгтя тут в том, что server optimized ping - это необязательная фича. В принципе ping может и не поддерживаться MUC-ом, насколько я видел urn:jabber:ping как фича MUC-ами не анонсируется ни в ejebberd ни в openfire. Из весёлого - при попытке попинговать сам MUC в jabber.ru происходит дисконнект. А должно бы по идее возвращаться IQ error not-implemented или IQ error not-available, во всяком случае так написано в xep-0199.

Продолжаем тему xmpp-протокола. Естественным и натуральным образом это асинхронный протокол. В чём прикол? А в том, что, например, во время между запросом на discover и ответом на него могут прилететь другие события, допустим, смена статуса или пинг. То же самое и с пингом. Особенно весело на этом фоне выглядит пинг MUC-а по "всратому" сценарию. Или вот - мы хотим поставить presence в чятике, отправили запрос на джойн к чятику и... нам надо дождаться, пока к нам прилетит наш же presence, но пустой - это значит, что мы заджойнились и только после этого можно выставлять нужный presence. Если мы поторопимся и умудримся присунуть серверу presence раньше времени, до того, как он зарегистрирует нас в комнате, то он вполне может нам ответить IQ error not-found, согласно стандарту - имеет полное право (и должен именно так делать).

То есть отправка сообщений и приём сообщений на стороне клиента просто обязаны быть реализованны асинхронно. А раз они асинхронные, то определение таймаута того же пинга подразумевает, что мы сохраняем время отправки ping-а, в момент приёма pong-а мы сохраняем время приёма pong-а, а потом также асинхронно (либо при отправке следующего пинга) вынимаем оба эти времени и сверяем на предмет а не было ли таймаута? А если понг ещё не пришёл, то мы проверяем время ping-а на предмет всё того же таймаута... (кстати, у пинга и понга по идее должны быть одинаковые id, этим можно оперировать, подставляя туда, например, время отправки оригинального пинга). Вобщем, нужно глобальное состояние и... глобальные переменные.

А что у нас с глобальными переменными? а они - это фу-фу-фу, плохой паттерн, вы не умеете программировать итд. Поборники pep-ов (а-ля pep-8) сейчас бы начали срать кровавым поносом. Однако, нас глобальными переменными смутить невозможно.

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

Или вот - из прекрасного. По стандарту, если клиент выходит из чятика, он должен отправить presence type unavailable. А как вы думаете, что произойдёт с другими клиентами с тем же jid-ом, если они в чятике? Они автоматом перестанут получать из него сообщения. То есть их "выйдут" вместе с тем клиентом, который завершает свою работу. А это значит, что надо во-первых, перед выходом проверять, кто сидит с чятике - нету ли нас же, но с другим ресурсом или с другим priority и посылать этот самый presence type unavailable, только если мы единственный клиент, а во-вторых следить, если нам прилетело сообщение, что нас откуда-то вышли, то проверять "а должны ли мы там быть?" и если должны, то реджойниться. Вобщем, стандарт стандартом, а здравый смысл ещё никто не отменял. Да и парактический подход тоже.

И такие спорные моменты в протоколе xmpp встречаются не только в вышеописанных ситуациях.

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

Next Post