Terry Architecture
High-Level View
Terry описывает платформу, в которой ingestion и delivery разделены на независимые capability blocks:
- adapters для чтения из
source; - orchestrator для запуска
ingestion job; - validation layer для rule execution;
- transformation layer для приведения данных к internal shape;
- persistence layer для записи в
sink; - notification layer для рендеринга шаблонов и отправки в
delivery channel.
Logical Components
flowchart LR
S["Source adapters"] --> J["Ingestion job runner"]
J --> V["Validation layer"]
V --> T["Transformation layer"]
T --> P["Persistence / sink writers"]
P --> N["Notification engine"]
V --> E["Error envelope / dead-letter"]
T --> E
P --> E
N --> D["Delivery channels"]
Responsibilities
Source adaptersинкапсулируют transport-specific extraction: HTTP, polling, scraping, files, queues.Ingestion job runnerподнимает execution context, применяет job config и управляет retries/timeouts.Validation layerотделяет hard-fail ошибки от soft-fail сигналов и возвращает структурированный результат.Transformation layerпереводит source payload в normalizedrecordи добавляет derived fields.Persistence layerобеспечивает идемпотентную запись и трассируемость источника данных.Notification engineреагирует на результат pipeline и рендерит сообщения по шаблонам.
Architectural Boundaries
Terry не должен документироваться как набор отдельных workflow/activity entrypoints. Этот стиль уже подходит текущему Temporal runtime, но для Terry важнее platform narrative:
- какие capabilities есть;
- как они связаны;
- где проходят contracts;
- где лежат operational guarantees.
Когда появится код, implementation-specific страницы можно будет линковать из Terry, но не заменять ими этот раздел.
Execution Model
Базовая модель исполнения:
- job может запускаться по API, расписанию или внешнему событию;
- обработка может быть synchronous на ingress, но основной pipeline считается asynchronous;
- каждая стадия должна иметь correlation identifiers и execution metadata;
- ошибки и повторные попытки описываются отдельным envelope, а не теряются в логах.
Storage and Delivery Concerns
sinkможет быть разным, но normalizedrecordдолжен иметь стабильный internal shape;- persistence должна отделять raw payload, normalized result и delivery events;
- notification pipeline не должен читать source напрямую, только итог pipeline execution и associated payload.
Current Runtime Baseline
На текущем этапе Terry уже имеет минимальный runtime/config backbone в коде:
- config catalog хранится в PostgreSQL schema
terryв таблицахworkspaces,domains,data_sources,loader_configs; data_sourcesхранит source identity/policy и pointer на текущую активную config version;loader_configsхранит versioned runtime config и version-level statistics;- universal loader вызывается по
data_source_id, читает конфигурацию из catalog-а и строит execution input внутриLoaderActivity; - source selection по
domain + page_type, write-side config flows, execution history и notification orchestration пока не реализованы.
Reference ERD
Ниже canonical ERD для первого прохода Terry. Это target-state schema narrative: часть сущностей уже реализована, часть остаётся архитектурным направлением.
erDiagram
%% --- ДОМЕН: ИСТОЧНИКИ И НАСТРОЙКИ (PostgreSQL) ---
WORKSPACE ||--o{ DOMAIN : "владеет"
DOMAIN ||--o{ DATA_SOURCE : "включает"
DATA_SOURCE ||--o{ LOADER_CONFIG : "версии конфигурации"
DATA_SOURCE ||--|| TRANSFORMER_CONFIG : "имеет"
TRANSFORMER_CONFIG ||--o{ DATA_SOURCE_FIELD : "определяет поля"
%% --- ДОМЕН: ПРОКСИ-СЕРВЕРЫ (PostgreSQL / Redis) ---
PROXY_GROUP ||--o{ PROXY_SERVER : "содержит"
PROXY_SERVER ||--o{ EXCLUDED_PROXY : "временно блокируется в"
DATA_SOURCE ||--o{ EXCLUDED_PROXY : "имеет блокировки"
%% --- ДОМЕН: ВЫПОЛНЕНИЕ И ОРКЕСТРАЦИЯ (Temporal / PostgreSQL) ---
DATA_SOURCE ||--o{ JOB_INSTANCE : "порождает"
JOB_INSTANCE ||--o{ RAW_DATA_FILE : "скачивает"
JOB_INSTANCE ||--o{ STRUCTURED_DATA : "извлекает"
JOB_INSTANCE ||--o{ NOTIFICATION : "генерирует"
%% --- ДОМЕН: ПРАВИЛА И УВЕДОМЛЕНИЯ (PostgreSQL) ---
DATA_SOURCE ||--o{ RULE : "триггерит"
RULE ||--o{ SUBSCRIBER : "уведомляет"
SUBSCRIBER ||--o{ NOTIFICATION : "адресовано"
%% --- ОПИСАНИЕ СУЩНОСТЕЙ ---
WORKSPACE {
uuid id PK
string code_name "Например: terry"
string name
string description
boolean is_active
}
DOMAIN {
uuid id PK
uuid workspace_id FK
string code_name "Например: cbr.ru"
string name "Центральный банк РФ"
string description
int rate_limit "Разрешенная частота запросов"
}
DATA_SOURCE {
uuid id PK
uuid domain_id FK
string code_name "Например: cbrf_onDate"
string name
string page_type "Логическая страница или endpoint family"
string source_type "http, playwright, file, database, sdk, custom"
boolean is_historical
int expiration_days "Срок хранения сырых данных"
int priority
boolean is_active
uuid current_loader_config_id FK
}
LOADER_CONFIG {
uuid id PK
uuid data_source_id FK
int version
string status "draft, active, archived"
jsonb loader_settings
jsonb auth_settings
jsonb validator_settings
jsonb freshness_settings
jsonb processor_settings
jsonb fingerprint
jsonb incremental
jsonb schedule_settings
bigint total_runs
bigint successful_runs
bigint failed_runs
timestamp last_run_at
timestamp last_success_at
timestamp last_failure_at
timestamp last_failure_start_date
}
TRANSFORMER_CONFIG {
uuid id PK
uuid data_source_id FK
string transformer_type "Например: transformer_xslt"
text xsl_template "Сам шаблон трансформации"
}
DATA_SOURCE_FIELD {
uuid id PK
uuid transformer_config_id FK
string expression_field_name "Поле для формул"
string structured_field_name "Имя поля в БД (Name, Curs)"
string structured_field_type "Тип (Date, Float, Text)"
string description
}
PROXY_GROUP {
uuid id PK
string name "Название группы прокси"
}
PROXY_SERVER {
uuid id PK
uuid proxy_group_id FK
string ip_address
int port
boolean is_active "Доступен ли физически"
}
EXCLUDED_PROXY {
uuid proxy_id PK, FK
uuid data_source_id PK, FK
string exclusion_reason "Причина (auto, manual)"
timestamp excluded_at "Время добавления в пул исключений"
}
JOB_INSTANCE {
uuid id PK
uuid data_source_id FK
string status "Created, Requesting_loading, Success, Failed"
string result_code
text error_message
int notification_messages_count "Счетчик писем"
timestamp start_time
timestamp end_time
}
RAW_DATA_FILE {
uuid id PK
uuid job_instance_id FK
string file_path "Путь к объекту в MinIO"
string hash "SHA-256 скачанного файла (для дедупликации)"
timestamp request_time
timestamp expiration_time "Когда удалить файл"
boolean is_new "Флаг дубликата"
}
STRUCTURED_DATA {
timestamp time PK "Primary Key (TimescaleDB Time-Series)"
uuid data_source_id FK
uuid job_instance_id FK
jsonb data_payload "Распарсенные данные (Курс, Валюта и т.д.)"
}
RULE {
uuid id PK
uuid data_source_id FK
string name "Например: USD exchange rate change"
text expression "Формула: Abs(Round_down(...) - ...) > 0"
boolean is_active
}
SUBSCRIBER {
uuid id PK
uuid rule_id FK
string delivery_type "Email, Http_post"
string endpoint "URL вебхука или Email адрес"
}
NOTIFICATION {
uuid id PK
uuid job_instance_id FK
uuid subscriber_id FK
string status "Pending, Sent, Failed"
string error_message
timestamp created_at
}
ERD Notes
WORKSPACE,DOMAINиDATA_SOURCEзадают ownership hierarchy и rate-limit boundary для внешних интеграций.DATA_SOURCE.expiration_daysэто config-level retention policy для raw payload artifacts и поэтому должен жить в source catalog.LOADER_CONFIGэто versioned runtime config entity; active version определяется черезDATA_SOURCE.current_loader_config_id.- История и stats живут на уровне
LOADER_CONFIG, чтобы Terry мог связывать regressions и rollout effects с конкретной config version. - Метрики вроде
total_req_count,failed_req_countиlast_failure_start_dateлучше трактовать как execution telemetry, а не как статическую конфигурацию источника. Их owner должен быть уJOB_INSTANCE/observability слоя, а не у catalog row. TRANSFORMER_CONFIGиDATA_SOURCE_FIELDпока не реализованы, но остаются частью planned declarative transform stack.JOB_INSTANCE,RAW_DATA_FILEиSTRUCTURED_DATAпокрывают execution trace от raw artifacts до normalized payload.RULE,SUBSCRIBERиNOTIFICATIONотделяют detection logic от delivery logic.PROXY_GROUP,PROXY_SERVERиEXCLUDED_PROXYфиксируют отдельный operational subdomain, который не должен смешиваться с core contracts pipeline.