Думать - больно: нужно прилагать усилия и тратить энергию, чтобы разобраться. Поэтому, развитие идет, если сохранять status quo еще больнее.
Жуткая мешанина свойств, большая часть из которых не документирована, в которых дублируются данные в разных атрибутах, иногда там оказывается совсем не то, что ожидается (вместо номера может попасть логин терминала), поменяться местами (номер терминала с именем пользователя), может прийти вообще мусор, есть очень похожие атрибуты, которые легко перепутать (caller, callee). Из этих сырых данных нужно было собирать высокоуровневую историю:
1. внешний номер позвонил на наш номер
2. попал в голосовое меню, побродил и выбрал связаться с оператором
3. попал в очередь
4. из очереди звонок забрал оператор первой линии и ответил
5. оператор звонит техническому специалисту и консультируется
6. оператор соединяет оба вызова и отключается
7. технический специалист разговаривает и просит прислать факсом документ
8. внешний номер перенаправляется на войсмейл специалиста
9. отправляет факс
10. вызов завершен
Это, конечно, крайне экстремальный случай и большая часть вызовов будет состоять меньше чем из трех элементов, но держать в голове такие сценарии приходится (А. Купер: программисты оперируют тремя числами: 0, 1 и ∞).
Вводная
Для нас переломным моментом стал сбор записей о звонках. Мы подключается через 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 файл, который сам по себе был не слабой такой болевой точкой. В файле сначала идет набор событий, затем - набор записей. События засылаются в роутер и на выходе сравниваются с требуемыми (процесс сравнения заслуживает отдельного поста).
Хрупкий код
Хрупкий код приводит к хрупким тестам, и малейшие изменения в коде (добавлен новый атрибут) приводят к редактированию каждого из полусотни сценариев. Сейчас у нас уже четвертое ковровое переписывание сценариев, при этом обычный сценарий состоит из пятка событий на входе и парочки записей на выходе. Всё-же наличие тестов делает рефакторинг не таким страшным.
Продолжение с примерами кода