Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

uapg — хранение истории OPC UA в PostgreSQL/TimescaleDB

uapg — бэкенд историзации для сервера asyncua: реализует HistoryStorageInterface, пишет значения переменных и события в TimescaleDB и отдаёт их клиентам по HistoryRead.

pip install uapg              # сам бэкенд
pip install "uapg[crypto]"    # плюс чтение зашифрованной конфигурации

Требования: Python 3.12, asyncua 1.1.8 или 2.x, PostgreSQL с расширением TimescaleDB 2.x.

Быстрый старт

from datetime import timedelta
from asyncua import Server
from uapg import HistoryTimescale

server = Server()
await server.init()
idx = await server.register_namespace("http://example.com")

storage = HistoryTimescale(
    user="opcua", password="...", database="opcua", host="db",
    global_retention_period=timedelta(days=365),   # политика хранения TimescaleDB
)
server.iserver.history_manager.set_storage(storage)
await storage.init()          # схема, пул, буферы записи, надзор за соединением
await server.start()

temperature = await server.nodes.objects.add_variable(idx, "Temperature", 0.0)
await server.historize_node_data_change(temperature, period=None)

Схема создаётся сама при первом init() и приводится к нужному виду при каждом старте; повторный запуск ничего не ломает. Полный пример с событиями, узлами метрик и корректной остановкой — в examples/server_with_history.py.

Два класса

Класс События
HistoryTimescale хранятся целиком в events_history; фильтр HistoryRead применяется в памяти
HistoryTimescaleV2 дополнительно раскладываются по полям в таблицы поиска, фильтр выполняется в SQL

Это один и тот же бэкенд. HistoryTimescaleV2 отличается только тем, что включает поиск событий: режим берётся из аргумента events_storage_mode или из переменной окружения UAPG_EVENTS_STORAGE_MODE.

Режим Запись Чтение
legacy только events_history из events_history, фильтр в памяти
dual (по умолчанию) events_history + слой поиска через слой поиска, фильтр в SQL
v2 то же, что dual то же, что dual

events_history при этом не устаревшая таблица: из неё восстанавливается полный состав полей события, слой поиска хранит только то, по чему ищут. План перехода к одному хранилищу и критерии готовности — ADR-006.

Зачем фильтровать в SQL. Выборка ограничена числом значений, которое просит клиент. Если фильтр применяется после неё, клиент получает не «первые N подходящих», а «подходящие среди первых N» — и редкое событие в длинной истории не находится вовсе.

Какие поля индексировать и по каким разрешать поиск, решает установка:

from uapg import HistoryTimescaleV2
from uapg.v2.events_config import EventsV2Config
from uapg.v2.storage_mode import StorageMode

storage = HistoryTimescaleV2(
    ...,
    events_storage_mode=StorageMode.DUAL,
    events_v2_config=EventsV2Config.from_csv(
        indexed="dev_eui,serial",        # btree-индексы по колонкам
        filterable="dev_eui,serial",     # что разрешено фильтровать в SQL; пусто — всё
        aliases="devEui:dev_eui",        # имя в фильтре → имя колонки
    ),
)

События, записанные до включения слоя поиска, переносятся порциями: await storage.run_events_backfill(batch_size=500) — вызывать, пока backfill_lag_rows не станет нулём. Прогресс отслеживается по отметке в uapg_backfill_state, а не полным сопоставлением таблиц, поэтому не зависит от размера истории; глубину пробы и время жизни её результата задают events_backfill_probe_rows и events_backfill_status_ttl_sec.

Что поддерживается из OPC UA Part 11

HistoryRead для сырых значений (ReadRawModifiedDetails) и событий (ReadEventDetails), включая точки продолжения и чтение «от свежих к старым», когда время начала не задано или диапазон перевёрнут. Фильтр событий понимает Equals, Like, InList, IsNull, And, Or в SQL; остальные операторы ContentFilter вычисляются в памяти.

ReadProcessedDetails (агрегаты), ReadAtTimeDetails и HistoryUpdate asyncua отвергает в своём диспетчере раньше, чем обращается к бэкенду, поэтому они не поддерживаются ни одним бэкендом asyncua. Агрегаты — в планах (ADR-003).

Надёжность

Каждое правило ниже появилось после разбора реальной остановки историзации.

  • Все ожидания ограничены: запрос, флаш, создание пула, ожидание замка реконнекта. Молчаливый обрыв сокета ловится на уровне ОС через TCP keepalive и TCP_USER_TIMEOUT — прикладная отмена уходит в тот же мёртвый сокет.
  • Батч не теряется на первом сбое: флаш после обрыва или таймаута повторяется один раз на новом соединении. Одиночный запрос по таймауту не повторяется — это только удвоило бы ожидание.
  • Ошибка SQL не рвёт пул: если ответил сам сервер (синтаксис, ограничение), соединение исправно, и пересоздавать его незачем.
  • Отменённый флаш освобождает сервер: соединение обрывается, а его backend в PostgreSQL завершается сразу, а не держит блокировки до конца запроса.
  • Очередь записи ограничена: при недоступности БД лишние значения отбрасываются и считаются в метрике; сообщение об этом пишется не чаще раза в минуту. Воркер, умерший или залипший во флаше, поднимается заново.
  • Один плохой отсчёт не уносит пачку: значения с кодом качества Bad и без меток времени записываются корректно или отвергаются поштучно.
  • Старт не мешает записи: недостающий индекс создаётся по возможности, а зависшие сессии того же db_application_name после аварийного перезапуска снимаются.

Настройки

Все параметры — именованные аргументы конструктора; значения по умолчанию подобраны под прод.

Группа Параметры
Подключение user, password, database, host, port, schema, sslmode, min_size, max_size, db_application_name
Запись history_write_batch_enabled, history_write_max_batch_size, history_write_max_batch_interval_sec, history_write_queue_max_size, history_write_durability_mode (async/sync), history_write_read_consistency_mode (local/global)
Таймауты db_query_timeout_sec, db_command_timeout_sec, db_pool_create_timeout_sec, db_pool_close_timeout_sec, db_lock_wait_timeout_sec, history_flush_timeout_sec, history_worker_stall_timeout_sec
Keepalive db_tcp_keepalive_idle_sec, db_tcp_keepalive_interval_sec, db_tcp_keepalive_count, db_tcp_user_timeout_sec
Кэши history_last_values_cache_enabled, history_last_values_cache_max_size_mb, history_last_values_init_batch_size, history_metadata_cache_enabled, history_metadata_cache_init_max_rows
Хранение global_retention_period — политика TimescaleDB для всех таблиц истории, включая слой поиска событий; меняется без перезапуска через reapply_global_retention_policy()

Таймаут 0 или None означает «без ограничения».

Метрики и узлы OPC UA

get_performance_metrics() отдаёт снимок без обращения к БД: состояние очередей, длительности флашей, таймауты и реконнекты, текущие настройки. Ключевые показатели для диагностики: queue_fill_ratio и dropped_total — backpressure и потери; seconds_in_current_flush — отличает залипший флаш от простоя; timeouts_total, reconnects_total — доступность PostgreSQL.

Те же метрики и настройки можно опубликовать в адресном пространстве:

await storage.expose_history_settings_nodes(server, idx)   # Server/History/HistorySettings
await storage.expose_history_metrics_nodes(server, idx)    # Server/History/HistoryMetrics
await storage.refresh_history_metrics_nodes()               # обновлять периодически

Имена и типы узлов стабильны между версиями и проверяются тестами.

Зашифрованная конфигурация

HistoryTimescale.from_config_file(path, master_password) читает параметры подключения из файла, зашифрованного Fernet (нужен uapg[crypto]). Ключ лежит в отдельном файле .db_key, и при его наличии мастер-пароль в расшифровке не участвует: файл ключа рядом с файлом конфигурации равносилен открытым учётным данным. Храните их раздельно и не коммитьте.

Хранение

Таблицы ядра: variables_history и events_history (гипертаблицы TimescaleDB), variable_metadata, event_sources, event_types, variables_last_value. Слой поиска событий: events_ts и по таблице evt_<тип> на каждый тип события.

Период хранения один на все гипертаблицы и задаётся global_retention_period: отдельной политики у слоя поиска нет и быть не должно — строка поиска без своего события в events_history означала бы, что фильтр его находит, а HistoryRead не отдаёт. При старте политика сверяется с настройкой и исправляется.

Переход с 0.2.x

Схема БД не меняется: 0.3 работает с существующей базой без миграции. Публичный API HistoryTimescale и HistoryTimescaleV2 сохранён.

Из пакета удалены HistoryPgSQL (бэкенд без TimescaleDB), DatabaseManager, create_database_standalone, backup_database_standalone и CLI python -m uapg.cli. Проекты, которые ими пользовались (opc-vibroiot-db), должны взять этот код из версии 0.2.15 (main, коммит 18df1cf) к себе. Подробности — в CHANGELOG и ADR-005.

Разработка

uv venv && uv pip install -e ".[dev]" pytest-asyncio
docker compose -f docker-compose.test.yml up -d    # одноразовая TimescaleDB на 55432
pytest                                             # юнит, контракт и интеграция
mypy src/

Без поднятой БД интеграционные тесты пропускаются. Контрактные тесты сверяют публичный API, схему и формат записи с замороженным снимком 0.2.15 в tests/contract/baseline/ — любое отступление от него должно быть явно перечислено в tests/contract/divergences.py с причиной.

Устройство пакета:

src/uapg/
  history_timescale.py     фасад: разбор вызовов OPC UA, публичный API
  history_timescale_v2.py  тот же бэкенд с поиском событий по умолчанию
  core/      настройки, соединение и реконнект, метрики, буфер записи, SQL
  codec/     формат записи значений и событий, ключи NodeId
  storage/   схема, переменные, события, слой поиска, кэши
  opcua/     границы чтения, фильтр событий, публикация узлов
  sql/       весь SQL: схема, миграции, запросы

Лицензия

MIT.

Metadata

Release files for uapg 0.3.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for uapg 0.3.0rc1
File Size Uploaded
uapg-0.3.0rc1.tar.gz 97.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for uapg 0.3.0rc1
File Interpreter ABI Platform
uapg-0.3.0rc1-py3-none-any.whl Python 3 none any Details

Total release size: 211.3 kB

Release files / uapg-0.3.0rc1.tar.gz

Download URL uapg-0.3.0rc1.tar.gz
Size 97.1 kB
Tags Source
SHA-256 checksum
How to use checksums
22dd2506730e52d6f1c53303a253e9f556533976e64f003898bb63f4103e7c93
BLAKE2b-256 checksum
How to use checksums
ad0817fe2d27c0fdea0e0673051d8366b9d0e22e8d489ccd3f4afcb3d1a2af90
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / uapg-0.3.0rc1-py3-none-any.whl

Download URL uapg-0.3.0rc1-py3-none-any.whl
Size 114.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
797b90df0e2fd4094a8327e920c0707fb675f0e2768179ab6b78d304109fdf39
BLAKE2b-256 checksum
How to use checksums
c42157cb717bd9a921e3b7bf2e40aaf0261c8aa07b385bc7e445ecaa909cfaca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page