Загрузка данных¶
Обмен с порталом сосредоточен в src/soniks_client/api.py; куда после этого
девается файл, решают модули src/soniks_client/jobs/.
Точки обмена¶
Функция |
Метод |
Куда |
Когда |
|---|---|---|---|
|
|
|
Раз в минуту — расписание проходов |
|
|
|
Из очереди выгрузки — версия клиента и метаданные, полями формы, не |
|
|
|
Каждый файл, |
|
|
|
После первой успешной сверки расписания, до первого успеха — режимы из |
Все запросы идут через одну requests.Session с ретраями: до четырёх попыток,
паузы 0/2/4 с, только на PUT и только на временные отказы (429, 500, 502,
503, 504). 404 и 403 «has already been uploaded» не повторяются — это
осмысленные ответы портала. GET расписания не повторяется тоже: он и так
идёт раз в минуту, а четыре попытки по 45 с съели бы
SCHEDULER__MISFIRE_GRACE_TIME_IN_SECOND и раздули бы last_sync_age
в /healthz. Настоящий долгий backoff — это очередь incomplete/, а не
urllib3.
Аутентификация — заголовок Authorization: Token <STATION__TOKEN> во всех
запросах. Запрос расписания дополнительно передаёт координаты станции (портал
считает видимость проходов на своей стороне) и параметр capabilities —
перечисление того, что этот клиент понимает в ответе, из константы
CAPABILITIES.
capabilities — это опт-ин, и он существует ради станций, которые уже не
обновятся. Портал отдаёт новое поле только тому, кто его попросил, поэтому
станция старой версии получает ответ байт-в-байт как раньше и сломаться от
правки портала не может. Добавлять сюда имя можно только тогда, когда клиент
действительно умеет обращаться с полем: заявление честно по построению — клиент
и flowgraph_dispatcher едут в одном образе. Подробности и мотив —
Дорожная карта: надёжность сети, решение 21.
upload_observation_data() отправляет по одному файлу за запрос. Имя поля
формы зависит от типа данных: payload, waterfall или demoddata.
MIME-тип угадывается по имени файла, с запасным вариантом
application/octet-stream.
Три сценария выгрузки¶
Во время прохода¶
send_data_during_observation() вызывается наблюдателем файловой системы
(CreateFileHandler) по мере появления файлов data_*. Кадры, возникшие за
окно OBSERVATION__BATCH_DELAY, отправляются одной пачкой.
Это не оптимизация, а страховка: проход длится минуты, и если ждать его конца, обрыв связи или падение контейнера унесёт всю телеметрию. Данные оказываются на портале ещё до захода спутника.
После прохода¶
send_data_after_observation() — одноразовое задание, поставленное в конце
execute_observation. Собирает файлы директории наблюдения по префиксам,
пропуская пустые, и выгружает каждый своим обработчиком:
Префикс |
Обработчик |
Содержимое |
|---|---|---|
|
|
JSON метаданных прохода, уходит полем формы |
|
|
Аудио |
|
|
PNG водопада |
|
|
Кадр: base64-поле |
Состав определяется OBSERVATION__UPLOAD_TYPES__*: аудио и водопад можно
отключить, кадры и метаданные выгружаются всегда.
Метаданные попали сюда не сразу. Раньше постобработка отправляла их одним
PUT прямо из Observation.post_processing(), и это был единственный путь
«выстрелил и забыл»: при ошибке сети блок signal (SNR, девиация, ppm)
исчезал навсегда — повторять было нечего и неоткуда. Теперь постобработка
пишет metadata_<id>_<время>.json в директорию наблюдения и на этом
заканчивается; дальше файл живёт по общим правилам. Повтор безопасен без
content-hash: портал перезаписывает поля наблюдения, а не отвергает дубль, —
403 «has already been uploaded» к метаданным не относится.
Отправитель выбирается по тому же префиксу, что и читатель: метаданные уходят
полями формы, остальное — multipart. Префикс, добавленный в
get_prefixes_of_files_available_for_upload() без записи в
_handlers_by_prefix(), роняет задание выгрузки с KeyError — обе правки
всегда в одном коммите.
Повторная отправка¶
resending_jobs() раз в SCHEDULER__RESENDING_INTERVAL_IN_MINUTES (20 минут)
проходит по incomplete/ и ставит задание send_observation_data_again() на
каждую директорию наблюдения. Тип файла определяется по префиксу имени.
Директория закрывается, только когда ушли все файлы. Если хоть один не отправился, в лог идёт предупреждение, и попытка повторится в следующий цикл.
Судьба файла зависит от исхода выгрузки¶
Это центральное соглашение всего модуля. Исключения из core/exceptions.py
здесь не «ошибки» в обычном смысле — они сообщают, что делать с файлом:
успех
├─ OBSERVATION__REMOVE_OBSERVATION_DATA=True → файл удаляется
└─ False → файл переезжает в complete/
FileNotUploadedError портал недоступен, сеть, таймаут, любой другой код
└─ файл → incomplete/, повтор раз в 20 минут
FileRejectedError 400: портал отверг файл, те же байты получат тот же ответ
├─ первая попытка (во время и после прохода) → incomplete/, как выше
└─ повторная отправка, снова 400 → файл закрывается как выгруженный:
удаляется либо в complete/
ObservationNotFoundError 404: наблюдение удалено на портале
├─ первая попытка (во время и после прохода)
│ └─ файлы → incomplete/, отправка прекращается
└─ повторная отправка (уже из incomplete/)
└─ вся директория наблюдения удаляется
По этому дереву ходят все артефакты прохода, метаданные в том числе: файл
удаляется только по подтверждённому приёму. Застрявшие метаданные держат
директорию в incomplete/, а значит видны снаружи — /healthz считает именно
директории.
Логика ObservationNotFoundError неочевидна, но верна: если проход удалён на
портале, отправлять больше нечего и некуда. Продолжать перебор файлов — значит
получить 404 на каждый; поэтому обработчик выходит сразу, а не continue.
Удаление при этом отложено на одну попытку. Один 404 — слишком слабое
основание стирать единственную копию данных: он может прийти и от временной
ошибки на стороне портала. Поэтому первый 404 только откладывает файлы в
incomplete/, а удаляет их повторная отправка, получившая 404 второй раз —
не раньше чем через SCHEDULER__RESENDING_INTERVAL_IN_MINUTES. Никакого
счётчика для этого не нужно: состояние — сам факт нахождения директории в
incomplete/.
400 устроен так же, как 404, — две ступени. Портал отдаёт его навсегда: имя
кадра не по контракту (malformed_filename) или метка кадра дальше пяти минут
от окна наблюдения (frame_outside_window, с 2026-08-28). Код ответа клиент не
разбирает — любой 400 на те же байты повторится. Первый отказ откладывает
файл в incomplete/: кратковременный 400 от ошибки портала, исправленной за
время между попытками, не должен стоить кадра. Повторный — закрывает файл по
OBSERVATION__REMOVE_OBSERVATION_DATA, как выгруженный, и остальные файлы
наблюдения больше не держит. До этого один отвергнутый кадр оставлял в
incomplete/ всё наблюдение и уходил на портал каждые 20 минут без предела
(решение 24 в Дорожная карта: надёжность сети).
frame_outside_window почти всегда означает ушедшие часы станции: метку кадра
ставит диспетчер по ним. Поэтому клиент на каждой сверке расписания сравнивает
свои часы с заголовком Date ответа портала: расхождение уходит в /healthz
полем clock_skew_seconds, а больше 60 секунд — ещё и предупреждением в лог.
Все три директории живут под PATHS__BASE. Оба compose задают его равным
/var/lib/soniks-client/data — постоянному тому, а не /tmp, который
примонтирован как tmpfs. До этого очередь incomplete/ не переживала
перезапуск контейнера, и окно повторной отправки существовало только пока
контейнер жив.
Обработка ошибок HTTP¶
Каждая функция api.py разбирает requests.Timeout, requests.HTTPError и
requests.RequestException по отдельности и логирует их с идентификатором
наблюдения. Наружу уходят только два «файловых» исключения, описанных выше.
get_observation_jobs() при любой ошибке возвращает None, а не бросает
исключение: недоступность портала не должна ломать цикл синхронизации — на
следующей минуте будет новая попытка.
Что стоит знать при доработке¶
data_to_send.popitem()вupload_observation_dataразрушает переданный словарь. Он рассчитан ровно на одну пару «поле → содержимое»; повторное использование словаря не сработает.Пустые файлы отбрасываются ещё на этапе сбора путей (
_collect_file_paths_by_prefix): нулевой размер — молча пропустить.Префиксы и имена полей формы — это настройки (
OBSERVATION__PREFIX__*,OBSERVATION__SENDING_KEY__*), а не константы в коде. Но портал ожидает конкретные значения, так что менять их на станции бессмысленно.Метаданные наблюдения включают все параметры потокового графа, см. Потоковый граф.