2013-03-26

Кровища и картины анальной боли

Думать - больно: нужно прилагать усилия и тратить энергию, чтобы разобраться. Поэтому, развитие идет, если сохранять status quo еще больнее.

Вводная

Для нас переломным моментом стал сбор записей о звонках. Мы подключается через ESL к Freeswitch и подписываемся на события которые льются непрерывным потоком вперемешку от разных событий. События крайне низкоуровневые вида: CHANNEL_CREATE (создан лег), CHANNEL_BRIDGE (два лега соединены), CHANNEL_HANGUP (лег завершен). Внутри каждого события имеется набор родных атрибутов, могут быть и наши, которые подсовываются в процессе генерации диалплана:

Content-Length: 1754
Content-Type: text/event-plain

Event-Name: CHANNEL_CALLSTATE
Core-UUID: f852daae-6da9-4979-8dc8-fa11651a7891
FreeSWITCH-Hostname: test
FreeSWITCH-IPv4: 1.2.3.4
FreeSWITCH-IPv6: %3A%3A1
Event-Date-Local: 2010-12-21%2014%3A21%3A54
Event-Date-GMT: Tue,%2021%20Dec%202010%2013%3A21%3A54%20GMT
Event-Date-Timestamp: 1292937714788536
Event-Calling-File: switch_channel.c
Event-Calling-Function: switch_channel_perform_set_callstate
Event-Calling-Line-Number: 213
Original-Channel-Call-State: HANGUP
Channel-State: CS_DESTROY
Channel-Call-State: DOWN
Channel-State-Number: 12
Channel-Name: sofia/internal_et/8000%40sipdomain.de
Unique-ID: 005f03fa-c803-428e-92cb-10534ac780dd
Call-Direction: inbound
Presence-Call-Direction: inbound
Channel-Presence-ID: 8000%40sipdomain.de
Answer-State: hangup
Channel-Read-Codec-Name: G722
Channel-Read-Codec-Rate: 16000
Channel-Read-Codec-Bit-Rate: 64000
Channel-Write-Codec-Name: G722
Channel-Write-Codec-Rate: 16000
Channel-Write-Codec-Bit-Rate: 64000
Caller-Direction: inbound
Caller-Username: 8000
Caller-Dialplan: LUA
Caller-Caller-ID-Name: Helmut%20Kuper
Caller-Caller-ID-Number: 8000
Caller-Network-Addr: 2.2.2.2
Caller-ANI: 8000
Caller-Destination-Number: ***6
Caller-Unique-ID: 005f03fa-c803-428e-92cb-10534ac780dd
Caller-Source: mod_sofia
Caller-Context: internal.lua
Caller-Channel-Name: sofia/internal_et/8000%40sipdomain.de
Caller-Profile-Index: 1
Caller-Profile-Created-Time: 1292937711184483
Caller-Channel-Created-Time: 1292937711184483
Caller-Channel-Answered-Time: 1292937711200482
Caller-Channel-Progress-Time: 0
Caller-Channel-Progress-Media-Time: 1292937711200482
Caller-Channel-Hangup-Time: 1292937714786536
Caller-Channel-Transfer-Time: 0
Caller-Screen-Bit: true
Caller-Privacy-Hide-Name: false
Caller-Privacy-Hide-Number: false

Жуткая мешанина свойств, большая часть из которых не документирована, в которых дублируются данные в разных атрибутах, иногда там оказывается совсем не то, что ожидается (вместо номера может попасть логин терминала), поменяться местами (номер терминала с именем пользователя), может прийти вообще мусор, есть очень похожие атрибуты, которые легко перепутать (caller, callee). Из этих сырых данных нужно было собирать высокоуровневую историю:
1. внешний номер позвонил на наш номер
2. попал в голосовое меню, побродил и выбрал связаться с оператором
3. попал в очередь
4. из очереди звонок забрал оператор первой линии и ответил
5. оператор звонит техническому специалисту и консультируется
6. оператор соединяет оба вызова и отключается
7. технический специалист разговаривает и просит прислать факсом документ
8. внешний номер перенаправляется на войсмейл специалиста
9. отправляет факс
10. вызов завершен

Это, конечно, крайне экстремальный случай и большая часть вызовов будет состоять меньше чем из трех элементов, но держать в голове такие сценарии приходится (А. Купер: программисты оперируют тремя числами: 0, 1 и ∞).

Решение

Главная проблема в том, что невозможно (proof me wrong) написать это stateless: высокоуровневая запись постепенно наполняется крохами данных, выдираемых из низкоуровневых событий. Отдельные события могут объединяться (6), из одной записи может рождаться другая (2), записи могут обмениваться информацией без слияния (8). Следовательно должен быть роутер, хранящий в себе состояние ведущихся звонков, передающий события для извлечения данных и опрашивающий на предмет готовности. Этот роутер должен быть крайне устойчивым: события могут не дойти из-за переполнения буфера, могут прийти лишние события, например, от потерянных ранее вызовов, иногда события приходят в неправильном порядке. Спасает то, что из потока событий можно извлекать паттерны размазанные по нескольким событиям: переводы вызова, звонки внутрь и наружу, работа с очередью и голосовым меню, конференции - главное все это правильно связать.

Fuzzy logic

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

Боль и унижение

В итоге у нас два слипшихся колобка спагетти правил с кучей проверок и переходов. Когда начали писать и сценариев был десяток это всё можно было протестировать руками. Но беглый набросок на бумаге выявил пол сотни. Код был чрезвычайно хрупок: малейшее изменение в логике при попытке починить сценарий херило корректную обработку всего остального. Время на добавление нового сценария росло экспоненциально (30 секунд на один сценарий * 50 сценариев = 25 минут). Плюс реальное тестирование не совместимо с отладкой: у сессии секундный таймаут и любая попытка поставить точку останова чтобы пройти по шагам и посмотреть, как роутятся события мгновенно разорвет сессию. Выбора не было - пришлось писать тесты. Каждый тест выглядел как YAML файл, который сам по себе был не слабой такой болевой точкой. В файле сначала идет набор событий, затем - набор записей. События засылаются в роутер и на выходе сравниваются с требуемыми (процесс сравнения заслуживает отдельного поста).

Хрупкий код

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

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