Tech stack
Два парка на одном main, обмен через SQLite WAL, жизнь задачи с пятью гейтами, разведка селекторов прежде кода, флаги, деплой и самопроверка системы.
TL;DR
Хаб на Node ставит задачи, пул воркеров на Python их выполняет, обмен идёт через общий SQLite в режиме WAL. Между «задача создана» и «палец коснулся экрана» стоят пять гейтов. Селекторы привязаны к версии приложения, и задача скорее упадёт, чем ткнёт наугад. Деплой идёт только через CI и умеет откатывать код, но не схему — и это сказано честно.
Технический стек
Два парка, один main
Репозиторий общий для двух ферм: локального USB-парка на семь телефонов и выделенной Windows-машины на девять. Постоянных веток под ферму нет — только временные под задачу. Фарм-специфичных значений в коде не бывает вовсе: хост, путь, таймзона, серийник устройства, почта — всё живёт в .env своей фермы.
Держат это две проверки в CI. Первая запрещает новые фарм-литералы в коде. Вторая требует, чтобы у каждого нового алиаса селектора стояла пометка, на какой ферме и на каком устройстве он разведан: алиас — это утверждение «я проверил этот билд на своём телефоне», а Pixel одной фермы не равен Samsung другой.
Почему обмен через базу, а не по HTTP
Хаб и воркер не ходят друг к другу по сети. Оба работают с одним файлом SQLite в режиме WAL, и у этого три следствия. Одна точка правды: состояние задачи не расходится между двумя процессами. Воркер переживает перезапуск хаба и наоборот — очередь лежит на диске, а не в памяти. И читатели не блокируют писателя, поэтому панель может опрашивать состояние сколько угодно часто, не мешая работе.
Цена решения — оба процесса обязаны жить на одной машине. Для фермы это не ограничение: телефоны всё равно висят на USB того же хоста.
Жизнь одной задачи
- Планировщик раскладывает задачи по неделе исходя из расписания персоны и весов активности — у каждой персоны свой профиль нагрузки.
- Задача ложится в очередь со временем запуска и приоритетом.
- Воркер клеймит её по замку на телефон: два процесса не могут одновременно трогать один экран.
- Перед выполнением проходят пять гейтов: окно активности персоны в её таймзоне; ночная пауза; парковка профиля; кулдаун телефона после тревоги; подтверждённая связь устройства с сетью.
- Задача выполняется под лизом с истечением. Если воркер умер, лиз протухает и задача возвращается в очередь — кроме последственных задач, где повторить вслепую нельзя.
- Исход пишется явно: выполнено, упало с причиной, пропущено или отложено в ручной разбор.
Последний пункт принципиальный. Если перезапуск оборвал задачу с необратимым действием, система не повторяет её никогда: прерванный тап мог уже дойти до площадки. Судьбу такой задачи выбирает человек.
Разведка прежде кода
Главная угроза для мобильной автоматизации — обновление приложения. Кнопка переехала, идентификатор сменился, и слепой скрипт начинает тыкать в чужие элементы: в лучшем случае открывает «Не интересно», в худшем — жалуется на чужой аккаунт.
Поэтому ни одна задача не пишется по угаданным селекторам. Порядок жёсткий:
- Селекторы живут отдельно и привязаны к версии сборки; в теле задачи идентификаторов нет вообще.
- Первая строка каждой задачи после подключения к устройству — сверка карты с экраном. Не совпало — исключение, и задача падает понятно.
- Сырые дампы экранов хранятся по версиям: 10 приложений, 75 сборок.
- Когда площадка выкатывает новый билд, сверка обязана сломаться. Это не баг, а замысел: хаб помечает версию устаревшей, и дальше идёт повторная разведка.
Поверх этого работает очередь самолечения: система сама снимает экран, предлагает правку карты и ждёт человека. Утверждает всегда человек.
Флаги
376 флагов с реестром, который знает зависимости между ними: часть флагов бессмысленна без предварительно взведённого соседа, и реестр это проверяет. Правило простое — новое поведение приезжает выключенным. Включение происходит сменой конфигурации на конкретной ферме, а не правкой кода, поэтому одна ферма может жить с новым поведением, пока вторая ещё на старом, и обе собираются из одного коммита.
Деплой
Деплой идёт только через CI: пуш в main → self-hosted runner → скрипт развёртывания по обратному SSH-туннелю. Ручной запуск скрипта запрещён регламентом, а пуш в main блокирует git-хук.
Цепочка выглядит так:
- Drain — кооперативный. Ставится файл-флаг, воркер перестаёт брать новое и доигрывает начатое. Ожидание до 46 минут, и по таймауту прерывается деплой, а не живая задача. Файл-флаг снимается ловушкой на любом выходе — страховка, поставленная после конкретного инцидента в июле.
- Снапшот базы через
VACUUM INTOс последующей проверкой читаемости нового файла. Провал проверки убивает деплой до того, как что-либо коснётся схемы. - Схема — аддитивная и идемпотентная, применяется при старте хаба. Версионированного раннера миграций нет, обратных миграций нет тоже.
- Health-gate — проверка, что поднявшийся хаб отвечает.
- Откат кода на предыдущий релиз при провале сборки, распаковки или установки зависимостей.
Честно о границах: на провале health-gate откат не запускается — деплой падает, а новый хаб остаётся раскатанным. Схема назад не откатывается по замыслу, поэтому безопасен именно откат старого кода на новую схему, а снапшот базы лежит на случай катастрофы и накатывается руками.
Система не верит своим отчётам
Это самая необычная часть проекта. Ежедневная сводка не перечисляет успехи — она ищет места, где успех был засчитан зря:
- Проглоченный рассинхрон — задачи, закрытые как успех или пропуск, при том что карта экранов уехала: падения нет, счётчики молчат, а действие не состоялось.
- «Экран подтверждён, а строк ноль» — само по себе не поломка, но именно так выглядит уехавшая карта, когда задачи при этом отчитываются успехом.
- Приравненные сборки, на которых продолжают падать — случаи, где машина приняла решение без человека и решение выглядит неверным.
- Повторные комментарии под одним постом — с оговоркой, что это вечный фон: стереть уже опубликованное ферма не умеет.
Под этим лежат 8173 теста в 358 файлах, 31 чекер контрактов и мутационные спеки, проверяющие, что тест действительно убивает мутанта, а не просто зелёный.
Честные пределы
- Масштаб упирается в физику: USB-порты, питание, место. Телефоны нельзя развернуть в облаке.
- Хаб и воркер обязаны жить на одной машине — цена обмена через файл базы.
- Ночная пауза и окна персон уменьшают дневную пропускную способность намеренно.
- Новая версия приложения останавливает работу на ней до повторной разведки. Это выбор в пользу аккаунта, а не в пользу метрик.