17 KiB
OPC Gateway — Design (SIENTIAPDE-1972)
- Épico: SIENTIAPDE-1972 — OPC Gateway: novo gateway derivado do OPC Ingestor, com ajustes no Orchestrator e no Laborious.
- Sub-tasks: 1973 (Gateway), 1974 (Orchestrator), 1975 (Laborious).
- Referências: 1879 (boas práticas OPC), 1569 (banco de tags / CRUD conectores).
- Branch:
SIENTIAPDE-1972-opc-gateway-novo-gateway-derivado-do-opc-ingestor-com-ajustes-no-orchestrator-e-no-laborious(no reposientia-dataops-opc-ingestor). - Data: 2026-07-13.
1. Contexto e objetivo
Evoluir o opc-ingestor (hoje read-only, leitura por subscription) para um OPC Gateway completo, cobrindo:
- Gestão de certificados (via MinIO + MongoDB — reaproveitando o que o épico 1569 já construiu).
- Leitura por subscription usando o sistema de slots já existente.
- Leitura ativa: agendada (frequência múltipla do ciclo de operação) e on-demand via GRPC.
- Escrita ativa via NATS como broker intermediário.
- Métricas detalhadas de todas as operações.
- Boas práticas / correções de bugs do card 1879.
O trabalho atravessa 3 repositórios. Este spec detalha o Gateway (repo atual) e define os contratos que o Orchestrator (1974) e o Laborious (1975) devem honrar. Cada um desses dois ganha spec próprio no seu repo.
2. Escopo
Dentro (este spec / este repo):
- Arquitetura interna do Gateway e todos os seus componentes.
- Consumo do banco de tags do 1569 e do slot v2 no Redis.
- Escrita OPC via NATS; leitura ativa agendada e on-demand via GRPC.
- Métricas e checklist 1879.
- Contratos C1–C4 (interfaces com Orchestrator/Laborious/chamadores GRPC).
Fora (specs próprios, outros repos):
- Implementação do 1974 no
orchestrator_temporal. - Implementação do 1975 no
laborious_temporal. - CRUD/UI do banco de tags (1569, já implementado).
3. Estado atual (fatos do código)
opc-ingestor (base)
asyncua1.1.5. Leitura só por subscription. Sem escrita, sem NATS, sem GRPC.- Slots vivem no Redis (
slot:opc_tags:{id}), com lease/heartbeat para balanceamento entre instâncias. - Arquitetura de managers:
opc_manager,ingestor_manager,data_manager,resource_manager. - Egress: Mongo (obrigatório) + Kafka (opcional). Métricas Prometheus em
:9090/metrics. - Conexão OPC hoje é criada por slot arrematado (só conecta nos servidores dos slots que possui).
dataops-library
MongoDBRepository(async:find/find_one/distinct/aggregate/insert/insert_many/update_one/update_many/delete_one/delete_many),MinioRepository,opc_ua_client, helpers de métricas Prometheus (sientia_do.observability). Métodos genéricos concluídos emfeature/SIENTIAPDE-1590.- Não tem NATS (usa Kafka), GRPC nem modelo de slot.
Banco de tags (épico 1569) — confirmado local em sientia-projects-templates @ rc/SIENTIAPDE-1588
Coleções no Mongo sientia:
| Coleção | Campos |
|---|---|
connectors (opc_ua) |
connector_id(int), type="opc_ua", name, url, server_uri, subscription_period_ms, use_security(bool), status, description |
connectors (rest_api) |
connector_id, type="rest_api", name, base_url, auth_config{type,token}, headers_config(JSON string), request_timeout, verify_ssl, status, description |
connector_tags |
tag_id(int), connector_id(FK int), address(NodeId), tag_name, category(Read/Write/Prediction Confidence), data_type(float/int/string/boolean), unit, status, description + atributos custom (top-level) |
platform_certificates |
doc único: client_cert{minio_object_name, sha256_hash, uploaded_at, original_filename, expiration_date}, private_key{...} |
Fatos relevantes:
- Segurança já é
Basic256Sha256(hardcoded em_create_opc_ua_client,connectors.py), com certificado de plataforma único (compartilhado), baixado do MinIO (bucketstreamlit-connectors, prefixoplatform/certificates/) para arquivos temp e passado aoasyncuaem runtime. - Multi-nome por endereço já é permitido: unicidade em
(connector_id, tag_name), nunca emaddress. - Não existe
metadata,model_id/model_name,collectionnemfrequencyemconnector_tags. Modelo/coleção/frequência vivem no ladomodels/pipelines(schema legado). O campometadataé trabalho do subtask 1600 (pendente). - Bug local conhecido:
projects-templates/app/src/operations/db_infra.py:16insere...\repos\sientia-dataops-librarynosys.path(caminho sem o subdirsoftware-engineer), inexistente nesta máquina → o app não importa a lib local. Questão em aberto (ver §12).
Escrita hoje (Laborious)
- Escrita OPC direta via
asyncua(laborious/activities/opc.py→opc_repository.write_data→node.write_value). - Status de escrita é gambiarra: sobrescreve
prediction_confidencecom sentinelas (12=erro escrita,14=sessão ruim; PI=13) emprocess_confidence. É esse acoplamento que o 1975 remove. - Payload de escrita já contém:
server_id,addr,value,data_type,tag_type(prediction|confidence),model_id,model_name,timestamp.
4. Arquitetura do Gateway (Abordagem B — engines por capacidade)
Orchestrator ─(Redis slot v2)─▶ SubscriptionEngine ─┐
Laborious ────(NATS write)────▶ NatsWriteConsumer ──▶ WriteEngine ────┐
caller ───────(GRPC read)─────▶ GrpcServer ─────────▶ ActiveReadEngine┤
scheduler ────────────────────────────────────────▶ ActiveReadEngine┤
▼
OpcConnectionPool (por servidor) ─▶ servidores OPC
│
DataSink ◀────────────────────┘
├─▶ Mongo (dados) / Kafka topics
└─▶ Mongo `models`.write_status
Decisão-chave: conexões OPC deixam de ser atreladas a slot. Escrita (NATS) e leitura on-demand (GRPC) podem mirar qualquer servidor, então a conexão vira um pool por servidor compartilhado por todas as engines.
Componentes
-
OpcConnectionPool(peça nova, núcleo compartilhado)- Conexões
asyncuaporconnector_id/server, reusadas por todas as engines; criação sob demanda + idle eviction. - Segurança
Basic256Sha256;secure_channel_timeout = int(session_timeout * 1.25)(1879 §3). - Certificados: baixa o platform certificate do MinIO (metadados em
platform_certificates), espelhando o padrão_create_opc_ua_clientdo 1569. - Uma única referência
time.monotonic()para latências/timeouts (1879 §2). Opção de sincronizar relógio via NTP. - Reconexão + métricas de conexão/desconexão e tipos de erro OPC.
- Conexões
-
SubscriptionEngine(evoluiingestor_manager+opc_manager)- Arremata slots no Redis (reusa lease/heartbeat), cria subscription por (slot, servidor) via pool.
- No
datachange, faz fan-out multi-nome: para cada{collection, tag_name, model_name, model_id}do endereço, emite registro aoDataSink.
-
ActiveReadEngine(nova)- Agendada: lê tags marcadas para leitura ativa em frequência múltipla do ciclo de operação; persiste via
DataSink. - On-demand: atende o
GrpcServer— lê agora, persiste E devolve valor+status+timestamp. - Mede lag de leitura (ts recebido − ts da máquina) (1879).
- Agendada: lê tags marcadas para leitura ativa em frequência múltipla do ciclo de operação; persiste via
-
WriteEngine(nova)- Escrita OPC via pool com
DataValue+Variant+status_codeGood +source_timestamp(1879 §1). - Flags: liga/desliga escrita de timestamp e de quality (quality
Good_Local_Override 0xD8se detectar problema de relógio). - Converter seguro: corrige BUG-1 (coerção de bool via string JSON) e BUG-2 (try/except em conversão de tipo), retornando "não escrito" + log em erro.
- Rate-limit / pausa mínima entre escritas.
- Diferencia tipos de erro OPC (métrica por erro).
- Após escrita, atualiza
models.write_statusviaDataSink. - (Futuro/flag) opção de escrever via método OPC em vez de tag direta (1879).
- Escrita OPC via pool com
-
Adapters de entrada
NatsWriteConsumer: subscribe no subject de escrita com queue groupgateway-writers(exatamente 1 instância processa cada comando). Core NATS (fire-and-forget); migração para JetStream fica para depois sem mudar o Laborious.GrpcServer: RPC de leitura on-demand →ActiveReadEngine.
-
DataSink(evoluidata_manager)- Persiste leituras no Mongo/Kafka com fan-out multi-nome.
update_model_write_status(model_id, status, ...)na coleçãomodels.
-
app— instancia pool + engines + adapters, sobe Prometheus, trata sinais e shutdown gracioso.
Coordenação: lease/heartbeat de slots no Redis seguem governando SubscriptionEngine e a leitura agendada. Escrita e leitura on-demand não dependem de lease — qualquer instância atende, conectando sob demanda pelo pool.
Meta de qualidade (1879 §Geral): ≤ ~500 linhas por arquivo, ≤ ~20 linhas por função.
5. Contratos
C1 — Slot v2 no Redis (slot:opc_tags:{id})
Orchestrator escreve, Gateway lê. Substitui topics:[str] por lista multi-nome por endereço, e carrega config de leitura ativa:
{
"server1": {
"server_id": "1",
"name": "server1",
"url": "opc.tcp://host:4840",
"server_uri": "urn:...",
"use_security": true,
"subscription_period_ms": 1000,
"tags": {
"ns=2;i=2": {
"frequency": "15000",
"active_read": { "enabled": true, "cycle_multiple": 3 },
"names": [
{"collection": "raw_x", "tag_name": "Counter", "model_name": "m1", "model_id": "1"},
{"collection": "raw_y", "tag_name": "Contador", "model_name": "m2", "model_id": "2"}
]
}
}
}
}
O Orchestrator monta isso fazendo o JOIN entre:
- Identidade da tag (banco de tags 1569):
connector_tags.address/tag_name/data_type/category+connectors.url/server_uri/use_security/subscription_period_ms. - Config de modelo:
model_id,model_name,collection,frequency(ladomodels/pipeline).
Multi-nome = múltiplas entradas em names para o mesmo address.
C2 — Mensagem NATS de escrita
Laborious publica, Gateway consome. Core NATS, subject opc.write (avaliar opc.write.{server_id}), queue group gateway-writers, fire-and-forget.
{
"msg_id": "uuid",
"server_id": "server1",
"addr": "ns=2;i=5",
"value": 42.0,
"data_type": "float",
"tag_type": "prediction",
"model_id": "1",
"model_name": "m1",
"timestamp": "2026-07-13T12:00:00Z",
"write_timestamp": true,
"write_quality": false
}
C3 — Campo de status de escrita (Mongo models)
Novo campo, substitui as sentinelas 12/13/14. Gateway grava após escrita OPC; Laborious grava nas escritas dele (PI/simples).
"write_status": {
"status": "ok|error|session_bad",
"opc_status_code": "Good",
"message": "...",
"source": "opc|pi",
"updated_at": "2026-07-13T12:00:01Z"
}
C4 — GRPC leitura on-demand
service OpcGateway {
rpc ReadTag(ReadTagRequest) returns (ReadTagResponse);
}
ReadTagRequest { string server_id; string addr; optional string data_type; }
ReadTagResponse { string value; string opc_status_code; string source_timestamp;
string server_timestamp; bool persisted; }
Lê agora, persiste no pipeline e devolve o valor.
6. Gestão de certificados
Reaproveita o que o 1569 já construiu: coleção platform_certificates (metadados) + MinIO (bucket streamlit-connectors, prefixo platform/certificates/). O OpcConnectionPool baixa client_cert/private_key para arquivos temporários e passa ao asyncua com Basic256Sha256, espelhando _create_opc_ua_client. Certificado de plataforma é único/compartilhado (não é por conector). Server cert opcional por conexão.
7. Métricas (1973 + 1879)
app_up; contadores de sucesso/erro de escrita e de leitura ativa; histogramas de latência de escrita e de leitura ativa; tags na subscription; slots possuídos; contagem de conexões/desconexões; status de escrita por modelo; valor de cada tag por tag/modelo; ciclos sem chegar dado em subscription; tipos de erro OPC diferenciados (contagem por erro); lag de leitura (ts recebido − ts da máquina). Base já existe em ingestor/metrics.py e em sientia_do.observability.
8. Checklist boas práticas (1879)
Basic256Sha256(nãoBasic256) — já é o padrão do conector; garantir no pool.time.monotonic()único para todo cálculo de tempo/latência.- Escrita com
status_codeGood +source_timestamp. - Flag liga/desliga escrita de timestamp.
- Flag liga/desliga escrita de quality (
Good_Local_Override 0xD8em problema de relógio). - Opção de sincronizar relógio via NTP.
- Rate-limit / pausa mínima entre escritas.
- BUG-1: coerção de bool a partir de string JSON (
str.lower() in ('false','0','no','off','')). - BUG-2: try/except na conversão de tipo do converter (retornar "não escrito" + log).
secure_channel_timeout = int(session_timeout * 1.25).- Diferenciar tipos de erro OPC nas métricas.
- (Flag/futuro) escrita via método OPC em vez de tag direta.
- ≤ ~500 linhas/arquivo, ≤ ~20 linhas/função.
9. Faseamento e dependências
- F0 — Fundações: scaffolding na branch;
OpcConnectionPool(cert MinIO+Mongo,Basic256Sha256, monotonic, channel>session); base de métricas. - F1 — SubscriptionEngine: consumo do slot v2 + fan-out multi-nome +
DataSink. (depende de C1) - F2 — WriteEngine + NatsWriteConsumer +
models.write_status. (depende de C2, C3) - F3 — ActiveReadEngine (agendada) + GrpcServer (on-demand). (depende de C4)
- F4 — Métricas completas + hardening 1879 + testes + Dockerfile.
Dependências externas:
- Banco de tags do 1569 disponível no Mongo alvo (schema
connectors/connector_tags/platform_certificates). - Broker NATS provisionado.
- Bucket de certs no MinIO (
streamlit-connectors). - Versão da
dataops-librarycom os métodos genéricos (feature/SIENTIAPDE-1590) publicada e pinada no gateway.
10. Trilhas paralelas (nível de contrato)
- Orchestrator (1974): montar slot v2 a partir do banco de tags (JOIN
connector_tags× modelo), propagarmodel_id/model_name/collection, suportar multi-nome por endereço. Arquivos-alvo hoje:orchestrator/utils/orchestrator_functions.py(gather_read_tags,build_tag_config),orchestrator/activities/formatters.py(process_slots),orchestrator/activities/slot_manager.py. - Laborious (1975): publicar comando de escrita no NATS (C2) em vez de
node.write_value; gravarmodels.write_status(C3) nas escritas próprias (PI/simples); remover as sentinelas12/13/14deprocess_confidence/api.py, desacoplando status de escrita da confidence.
11. Testes
- Unit por engine/pool (reusar padrão de
tests/unit/managers): pool (reconexão, cert), subscription (fan-out multi-nome), write (converter bool/erro, status_code/timestamp, rate-limit), active read (lag), adapters (NATS queue group, GRPC read+persist). - Cada lógica não trivial deixa ao menos um teste executável (converter, fan-out, error mapping).
- Funcional single-node contra o
opc_simulator.
12. Questões em aberto
- Campo
metadataemconnector_tags(subtask 1600, pendente): formalizar como campo nested. Nas tags de teste do gateway, criar commetadata: {}(pedido do Vitor, para documentação). - Durabilidade da escrita: começar com Core NATS puro; reavaliar JetStream (TTL curto p/ descartar comando obsoleto) se perda em restart se mostrar problema.
db_infra.py:16(projects-templates):sys.pathaponta para caminho inexistente (...\repos\sientia-dataops-library, faltasoftware-engineer). Corrigir para o app 1569 importar a lib local. Fora do escopo do gateway — decidir se entra aqui.- Origem de
collection/frequency/active_readno JOIN do 1974: confirmar de onde vêm (pipeline vs config de modelo) ao detalhar o spec do Orchestrator. - Chamador do GRPC on-demand: confirmar quem chama (Laborious? Orchestrator? UI/API) para dimensionar auth/limites do
GrpcServer.
13. Riscos
- At-least-once futuro (JetStream): escrita OPC não é idempotente por natureza; se migrar, tratar dedupe por
msg_id. - Relógio/OPC: timestamps e NTP são fonte recorrente de bug (1879) — priorizar
monotonice flags de quality. - Pool multi-instância: escrita/on-demand conectam sob demanda a qualquer servidor; vigiar limite de conexões e idle eviction para não estourar sessões no servidor OPC.
- Dependência de schema do 1569 ainda em
rc/: o schema do banco de tags pode mudar antes do merge; contrato C1 deve ser revalidado no merge.