Несколько городов на одном API

> Кратко. Один процесс API и общие воркеры обслуживают все города. Город запроса задаёт префикс пути /city/<код>/, базу — список TGIDCITYDBS. Схему базы города API доводит сам: после переконвертации из десктопа ничего не надо запускать вручную.

Читаю как

Два режима

Режим Как включается Город запроса База
Один город TGID_CITY_DBS не задан TGID_CITY, иначе по имени базы: almatygid → almaty, almatygid_copy → almaty DB_NAME
Несколько городов TGID_CITY_DBS=код=база,код=база,… префикс пути /city/<код>/ из списка

Пример: TGID_CITY_DBS=almaty=almatygid,shatura=shatura. Код города — латиница, цифры, _ и -, от 2 до 31 знака. Ошибка в списке (нет =, город указан дважды) останавливает запуск API с сообщением «TGID_CITY_DBS: …».

TGID_CITIES — справочник названий: код:Название[:путь веба], путь по умолчанию /tgid/<код>/. Без переменной действует встроенный список из 12 городов (cities.DEFAULT_CITIES). Справочник нужен для выбора городов у пользователя и для перехода между городами в вебе.

Город запроса

  • nginx отдаёт API города по его пути: location /tgid/<код>/map-api/ → http://127.0.0.1:<порт>/city/<код>/. Веб и его NUXT_PUBLIC_MAP_API_BASE_URL при этом не меняются.
  • Слой city_context.py снимает префикс /city/<код> и запоминает город. Маршруты, ссылки OGC и проверки путей остаются прежними.
  • Без префикса отвечают только /health, /api/health, /metrics и /auth/check. Остальное — 404 «Город не указан: путь API — /city/<код города>/…». Город не из TGID_CITY_DBS — 404 «Город «…» не обслуживается этим API».
  • Токен входа общий для всех городов. Доступ к городу (список городов пользователя) проверяется на каждом запросе.

Что разделено по городам

Что Как
Пул соединений Свой пул на город (DB_POOL_MIN_SIZE…DB_POOL_MAX_SIZE). Недоступная при запуске база открывается при первом запросе
Кеши процесса (каталог, графы сети, колонки журналов, версия данных карты, охват OGC) Ключ кеша — код города (cities.cache_key()); у API одного города — пустой
Очередь Celery tgid.<код>; выгрузки с FILE_JOBS_QUEUE=<очередь> — <очередь>.<код>. Задача несёт заголовок tgid_city
Результат и файл задачи Отдаются только запросу того же города
Доводка схемы базы Своя фоновая задача и своё состояние на город
Функции тайлов martin MARTIN_SQL_DIR/<код>.sql (у API одного города — MARTIN_SQL_FILE)
Статичные тайлы PMTiles STATIC_TILES_DIR/<код>/

Общие для всех городов: UsersDB (пользователи и журнал действий), база Redis (REDIS_DB), справочник sprav.

Размер пула. DB_POOL_MAX_SIZE действует на каждый город в каждом процессе API. По умолчанию 20: при 12 городах и двух процессах это до 480 соединений. Сверьте с max_connections PostgreSQL, на проде пул на город — 6.

Воркеры и очереди

  • Воркер слушает очереди, перечисленные в -Q. Общий воркер слушает все города (-Q tgid.almaty,tgid.astana,…). Для крупного города можно завести отдельный воркер (-Q tgid.almaty).
  • Перед задачей воркер берёт город из заголовка и открывает его базу. Задачу без известного города он не выполняет: это ошибка, а не расчёт на чужой базе.
  • Задача в очереди города, которую никто не слушает, ждёт бесконечно. Добавили город — добавьте его очередь воркерам.

Как добавить город

  1. Создайте базу города конвертером десктопа (gid8). Проверьте, что в ней есть таблицы сети nodes, linesobj, heatpipesections и externalcodes.
  2. Добавьте город в TGID_CITY_DBS службы API, а если его нет во встроенном списке — и в TGID_CITIES.
  3. Добавьте очередь tgid.<код> в -Q воркеров.
  4. Перезапустите API и воркеры. В ответе /health у города должно быть "ok": true, а city_schema.status — ok или applied.
  5. Опубликуйте слои города в GeoServer: рабочая область <Город>GIS, как у других городов. Для тайлов из martin сгенерируйте функции (scripts/martin/build_martin_functions.py) и положите их в MARTIN_SQL_DIR/<код>.sql.
  6. Веб: служба на город с NUXT_APP_BASE_URL=/tgid/<код>/, NUXT_PUBLIC_MAP_API_BASE_URL, охватом NUXT_PUBLIC_CITY_BBOX / NUXT_PUBLIC_MAP_CENTER и префиксом NUXT_PUBLIC_STORAGE_PREFIX. Без префикса города на одном домене делят настройки в браузере.
  7. nginx: пути /tgid/<код>/ (веб) и /tgid/<код>/map-api/ (API). Перед перезагрузкой проверьте конфигурацию nginx -t.
  8. Дайте пользователям доступ к городу: «Пользователи» → города пользователя. Пустой список — все города.
  9. Добавьте базу в список резервного копирования — Резервные копии.

Если у города своя система координат (не 3857, как у последних городов Казахстана), API о ней не знает. Нужен пункт в database/city_schema.py → CITIES и SQL с описанием SRID. Это правка кода, а не настройка.

Что API делает с базой города сам

База города приходит из десктопа и после каждой переконвертации теряет то, что добавил веб. API проверяет это при запуске и затем раз в DB_UPKEEP_INTERVAL_S секунд (по умолчанию 600, 0 — выключить) и применяет только недостающее. Каждый пункт применяется своим идемпотентным SQL из sql/, все вместе — одной транзакцией.

Что доводится:

  • функции tg_format* для слоёв GeoServer;
  • индексы: nodes_internalnodeid_idx, ut_out_lineid_calculationid_idx, пространственные индексы зданий;
  • таблицы API: журнал отмены и повтора правок, протоколы расчёта, износ, шаблоны карточек, авторство объектов («только свои»), сценарии, конфигурации трассировки, стили слоёв;
  • таблицы функций: наряды и фото, треки бригад, комментарии на карте, ключи встраивания, короткие ссылки, социальные объекты, отключения, вебхуки, присутствие на карте, формы обходов, телеметрия и тревоги, погода, водопровод, нормы сближений, глубина теплосети, прогноз аварийности, последствия аварий;
  • колонка generalizedconsumers.name;
  • Астана и Шатура — описание SRID 9998; Шатура — ещё колонка linesobj.internalnodeid с триггером;
  • функции тайлов martin, если задан MARTIN_SQL_FILE или MARTIN_SQL_DIR.

Когда API ничего не трогает:

  • база ещё не готова: нет таблиц сети или нужной системы координат (not_ready);
  • в базе идёт запись: конвертер работает или висит транзакция (busy);
  • другой процесс уже доводит эту базу (блокировка pg_try_advisory_xact_lock).

Во всех этих случаях API повторит попытку в следующий проход. Если применить не удалось, транзакция откатывается, база остаётся как была, а в /health у города city_schema.status = "error" с текстом ошибки.

UsersDB API сам не мигрирует: новые миграции alembic применяются при выкатке — Выкатка и откат.

Если не получается

  • 404 «Город не указан» — запрос пришёл без /city/<код>/. Проверьте proxy_pass в nginx: адрес должен заканчиваться на /city/<код>/.
  • Расчёт или выгрузка города «висит» в очереди — ни один воркер не слушает tgid.<код>.
  • city_schema.status: "busy" долго — в базе висит транзакция (idle in transaction) или идёт конвертация. Найдите её в pg_stat_activity.
  • Объекты города «уехали» — после переконвертации нет описания SRID 9998. API вернёт его в ближайший проход; /health покажет applied.

Для разработчика

  • Город: cities.current_city(); база — cities.city_db_name(), а не os.getenv("DB_NAME"). Новый кеш процесса, построенный по базе, — только с ключом cities.cache_key(), иначе данные одного города попадут в другой.
  • Фоновая работа вне запроса — внутри with cities.use_city(код):; очередь задачи — cities.task_queue().
  • Новый пункт схемы — Item в database/city_schema.py и SQL-файл в sql/, а не скрипт для администратора.
  • Подробности и проверка на стенде — docs/multi-city.md в репозитории API.
Хотите увидеть эту функцию на своих данных?Покажем ТГИД на демонстрации и ответим на вопросы инженеров.
Запросить демо