# Offline-First em FPSO: Sincronizar Dados Quando o Link de Satélite Cai a Meio do Turno
TL;DR: Numa FPSO, a aplicação de campo tem de assumir que o link vai cair — a arquitetura correta grava sempre localmente primeiro, sincroniza apenas as diferenças quando o link volta, resolve conflitos campo a campo (não turno a turno) e usa chaves de idempotência para que um reenvio nunca duplique uma ordem de trabalho ou uma leitura de tanque.
O problema não é a internet, é a suposição
A maior parte do software empresarial — ERPs, sistemas de manutenção, plataformas de HSE — é escrito a partir de um pressuposto implícito: o pedido HTTP chega ao servidor, o servidor responde, e se algo falhar o utilizador tenta outra vez daí a um segundo. Esse pressuposto é razoável num escritório em Luanda com fibra. É falso numa FPSO, onde a única ligação à terra passa por um link de satélite partilhado entre dados operacionais, voz, IPTV da tripulação e, cada vez mais, videoconferência.
Quando esse link cai — chuva forte, perda de apontamento da antena com o mar, congestionamento, ou manutenção do operador de satélite — a equipa de bordo não pode parar de trabalhar. Um técnico continua a fechar ordens de trabalho. Um operador de processo continua a registar leituras de tanques. Um supervisor de HSE continua a abrir observações de segurança. Se a aplicação só sabe funcionar com o servidor a responder, o resultado é papel, memória e reconciliação manual dias depois — o ponto onde os dados operacionais de uma unidade offshore se tornam pouco fiáveis.
A resposta de engenharia chama-se offline-first: a aplicação trata a rede como um recurso intermitente e opcional, não como uma dependência síncrona. Isto não é um detalhe de UX ("mostrar um ícone de offline"). É uma decisão de arquitetura de dados que atravessa o cliente, a fila de sincronização e o modelo de conflitos no servidor.
Porque é que o link cai — sem inventar números
Vale a pena perceber a física antes de desenhar a solução, sem forçar números que não se conseguem verificar para o caso concreto de cada unidade.
A maioria das FPSOs ainda depende de VSAT geoestacionário. Um satélite geoestacionário orbita a 35.786 km de altitude, e a distância percorrida pelo sinal impõe um atraso de propagação físico incontornável. Segundo o SatSig, um único salto — subir ao satélite e voltar a descer, cerca de 72.000 km — custa 240 ms no melhor caso, com o satélite mesmo por cima; na margem da área de cobertura, entre dois pontos igualmente distantes, sobe para cerca de 280 ms. O número que interessa a quem desenha software é o dobro disso: um pedido e a sua resposta atravessam o satélite duas vezes, ou seja 480 a 560 ms, antes sequer de somar o processamento em terra e a rede terrestre até ao centro de dados. É por isso que aplicações que dependem de resposta síncrona do servidor "sentem" o satélite mesmo quando o link está tecnicamente ativo.
A largura de banda também é historicamente escassa e cara no offshore de petróleo e gás: voz, um circuito de dados dedicado e videoconferência competem pelo mesmo canal, e a procura por dados tem crescido de forma constante à medida que mais sistemas passam a depender da ligação (Offshore Magazine). Serviços em banda Ku ou Ka ajudaram a aumentar a capacidade disponível, mas o link continua sujeito a interrupções por mau tempo, perda de apontamento da antena ou manutenção do operador de satélite.
Constelações de órbita baixa como a Starlink Maritime mudam o perfil de latência — por operarem muito mais próximo da Terra, a latência típica é inferior à dos sistemas geoestacionários tradicionais (Wikipédia — Starlink) — e já são usadas por armadores e operadores offshore como camada adicional. Mas isso não elimina o problema: continua a ser um serviço "best effort", sem garantia contratual de disponibilidade contínua, e a unidade continua a precisar de uma camada de resiliência que não dependa de nenhum link em particular. Essa camada é a aplicação em si.
A arquitetura: fila de escrita local primeiro
O princípio central do offline-first é simples de enunciar e trabalhoso de implementar corretamente: toda a escrita do utilizador é confirmada localmente antes de ser confirmada pelo servidor.
Na prática, isto significa:
1. Base de dados local no dispositivo ou no servidor de bordo. Cada tablet do técnico, ou um servidor local a bordo da FPSO, mantém uma cópia funcional dos dados operacionais relevantes — ordens de trabalho abertas, ativos, tanques, checklists de HSE — numa base de dados embutida (SQLite, ou um motor equivalente em dispositivos móveis).
2. Fila de escrita (write queue). Cada ação do utilizador — fechar uma ordem de trabalho, registar uma leitura, submeter uma observação de segurança — gera um registo imutável na fila local: quem, o quê, quando, e o payload da mudança. A aplicação nunca espera pela rede para confirmar a ação; confirma assim que grava localmente.
3. Processo de envio em segundo plano. Um processo separado drena a fila sempre que há conectividade — mesmo que seja uma janela de poucos minutos — e envia os registos pendentes para o servidor central, um a um ou em lotes pequenos.
4. Reconhecimento e limpeza. Só depois de o servidor confirmar a receção é que o registo sai da fila local. Até lá, permanece "pendente", visível ao utilizador se necessário.
Isto significa que a UI nunca bloqueia à espera do servidor, e que uma sessão de trabalho inteira — um turno de 12 horas com o link em baixo — não perde uma única ordem de trabalho fechada, desde que o dispositivo não seja perdido ou destruído.
Sincronização diferencial: nunca reenviar o mundo inteiro
Com largura de banda medida em poucos Mbps partilhados por toda a unidade, reenviar o estado completo da base de dados a cada reconexão não é opção. A sincronização tem de ser diferencial: enviar apenas o que mudou desde a última sincronização bem-sucedida, em ambos os sentidos.
Isto normalmente combina dois mecanismos:
- Vetores de mudança por registo. Cada entidade sincronizável (ordem de trabalho, leitura, ativo) tem um número de sequência ou timestamp lógico que é incrementado a cada alteração. O cliente guarda o "ponto de corte" da última sincronização e pede ao servidor apenas os registos com sequência superior — e vice-versa, o servidor pede ao cliente apenas os registos da fila local ainda não confirmados.
- Compactação de payload. Para campos de texto longo ou anexos (fotos de inspeção, por exemplo), enviar o delta binário ou comprimir agressivamente antes de colocar na fila, e adiar o envio de anexos pesados para quando a largura de banda disponível o permitir, sem bloquear a sincronização dos dados estruturados que são operacionalmente mais urgentes (estado da ordem de trabalho, por exemplo).
Isto reduz drasticamente o volume de dados por ciclo de sincronização, o que importa tanto pelo custo do link como pelo facto de que uma janela de conectividade offshore pode durar apenas alguns minutos antes do link cair de novo — a sincronização diferencial permite aproveitar essa janela para o que é operacionalmente crítico, em vez de a gastar a reenviar dados que não mudaram.
Resolução de conflitos: LWW, CRDT e merge por campo
Aqui está o problema real do offline-first, e onde a maioria das implementações amadoras falha: dois utilizadores diferentes podem alterar o mesmo registo enquanto ambos estão offline, e quando o link volta, o servidor recebe duas versões divergentes do mesmo dado. A pergunta "qual delas vence?" não tem uma resposta genérica — depende do tipo de campo.
Exemplo concreto
Considere a Ordem de Trabalho OT-4521, "Substituir vedante da válvula XV-204", numa FPSO com o link em baixo desde o início do turno.
- Às 08h14, o Técnico A regista no seu tablet: estado = Concluído, e consome 3 unidades de vedante do armazém de bordo.
- Às 11h40, o Supervisor B, sem saber que A já tinha fechado a ordem, regista na sua própria sessão: estado = Bloqueado — falta de peça de reserva, e não regista consumo de material.
O link volta às 14h00. O servidor recebe as duas mudanças quase em simultâneo. O que se passa?
Se o sistema usar last-write-wins (LWW) ingénuo — a versão com o timestamp mais recente vence — o registo do Supervisor B (11h40, mais tardio) sobrepõe-se ao do Técnico A. O resultado: a ordem fica marcada como "Bloqueado", apesar de ter sido fisicamente concluída, e os 3 vedantes consumidos desaparecem silenciosamente do histórico. Ninguém é avisado. É perda de dados disfarçada de resolução automática — o problema mais citado contra LWW ingénuo, precisamente porque o "vencedor" depende de relógios que podem estar dessincronizados entre dispositivos.
A alternativa correta combina duas estratégias, campo a campo:
1. Campos aditivos (contadores) resolvem-se com CRDT. O consumo de material — "3 unidades de vedante" — não é um valor que se sobrepõe, é um valor que se soma. Modelado como um contador CRDT (um PN-Counter, que separa incrementos e decrementos e converge sempre para o mesmo total independentemente da ordem de chegada), o consumo do Técnico A é preservado no inventário mesmo que o resto do registo entre em conflito. CRDTs garantem isto: réplicas que atualizam de forma independente, offline, fora de ordem, convergem sempre para o mesmo estado final sem coordenação prévia entre elas (crdt.tech).
2. Campos de estado com semântica de negócio escalam para revisão humana. "Concluído" e "Bloqueado — falta de peça" não são dois valores onde o mais recente é automaticamente o correto; são duas afirmações incompatíveis sobre o mesmo ativo físico. A prática defensável não é LWW nem merge automático — é detetar a divergência e apresentá-la como conflito explícito ao supervisor, com as duas versões lado a lado, para decisão humana. LWW continua aceitável para campos de baixo risco — uma nota de texto livre, por exemplo — mas é uma escolha de negócio, não um valor por omissão seguro para todos os campos.
Esta é a diferença entre "resolução de conflitos" como recurso genérico de biblioteca e resolução de conflitos como decisão de modelação de domínio: cada campo tem de ser classificado — aditivo, estado exclusivo, texto livre, anexo — e cada categoria tem uma estratégia diferente. Um comparativo direto entre LWW e CRDTs mostra o contraste: LWW é rápido e simples mas descarta silenciosamente a versão perdedora, enquanto CRDTs evitam essa perda ao estruturar a operação para que a fusão seja sempre determinística, ao custo de mais complexidade de modelação (DZone).
Idempotência: quando o cliente reenvia o mesmo pedido
Um link instável não produz apenas conflitos entre utilizadores — produz também reenvios duplicados do mesmo utilizador. O tablet do Técnico A envia o fecho da OT-4521, a ligação cai antes de a resposta chegar de volta, e a fila local — corretamente, do ponto de vista do dispositivo — interpreta a ausência de confirmação como falha e tenta de novo assim que o link regressa. Sem proteção, o servidor processa a mesma operação duas vezes: duas baixas de stock, duas notificações, possivelmente duas entradas de auditoria.
A técnica correta é a chave de idempotência: cada operação gerada na fila local recebe um identificador único (um UUID gerado no momento da ação do utilizador, não no momento do envio), que acompanha o pedido em cada tentativa, incluindo reenvios. No servidor, aceitar e aplicar essa mudança tem de ser uma operação atómica: reivindicar a chave e executar a mutação de dados na mesma transação, de forma que ou ambas acontecem ou nenhuma acontece — evitando a janela onde duas tentativas concorrentes veem a chave como "livre" ao mesmo tempo. Pedidos subsequentes com a mesma chave devolvem a resposta já processada, sem repetir o efeito. É a mesma prática documentada na AWS Builders' Library para APIs idempotentes sob retentativa (AWS) — o padrão que separa "reenviar com segurança" de "reenviar e duplicar a ordem de trabalho".
Um detalhe frequentemente esquecido: a chave tem de ser gerada a partir da intenção da operação (o momento em que o utilizador toca em "Concluir"), não a cada nova tentativa de rede. Se a chave for regenerada em cada retry, cada retry parece uma operação nova ao servidor, e a proteção de idempotência não serve de nada.
Onde isto se encaixa na infraestrutura
Nada disto funciona isoladamente no dispositivo. A fila de sincronização tem um destino — normalmente um serviço de ingestão na cloud, com filas geridas, armazenamento de eventos e observabilidade sobre taxas de conflito e latência de sincronização — e esse destino é tão crítico para a fiabilidade do sistema como a lógica no tablet do técnico. Desenhar essa camada (filas resilientes, deteção de duplicados, alertas quando a taxa de conflitos de um ativo sobe de forma anómala, e a topologia de rede entre a unidade offshore e o data center) é trabalho de infraestrutura cloud tanto quanto de aplicação — é o tipo de arquitetura que cobrimos no nosso serviço de Cloud & DevOps.
Checklist prático de implementação
- Toda a escrita do utilizador grava localmente primeiro e é confirmada na UI antes de qualquer chamada de rede.
- A fila de sincronização é durável (sobrevive a reinício da aplicação e do dispositivo) e ordenada por timestamp local.
- A sincronização é diferencial — nunca reenvia o estado completo, apenas os deltas desde o último ponto confirmado.
- Cada campo do modelo de dados está classificado: aditivo (CRDT), estado exclusivo (deteção de conflito + revisão humana), ou baixo risco (LWW aceitável).
- Toda a operação mutável tem uma chave de idempotência gerada no momento da intenção do utilizador, não no momento do envio de rede.
- A reivindicação da chave de idempotência e a mutação de dados acontecem na mesma transação atómica no servidor.
- Existe um painel — mesmo que simples — de conflitos pendentes de revisão humana, visível a quem gere as operações de manutenção ou HSE.
Perguntas Frequentes
Offline-first significa que a aplicação nunca precisa de rede?
Não. Significa que a rede é tratada como um recurso oportunista: a aplicação funciona plenamente sem ela para as operações do turno, mas depende dela para propagar dados entre a unidade offshore, outras unidades e os sistemas centrais em terra (ERP, CMMS, relatórios de HSE consolidados).
Porque não usar simplesmente last-write-wins em todos os campos, para simplificar?
Porque LWW descarta silenciosamente a versão perdedora sempre que dois utilizadores alteram o mesmo campo offline — o que pode significar perder o registo de que uma válvula ficou bloqueada por falta de peça, ou apagar consumo de material. É aceitável para campos de baixo risco, não para estado operacional ou quantidades.
CRDTs resolvem todos os conflitos automaticamente, sem intervenção humana?
Resolvem de forma determinística os conflitos para os quais a operação foi desenhada — tipicamente valores aditivos. Não resolvem, por natureza, conflitos de estado exclusivo com semântica incompatível (concluído vs. bloqueado); esses precisam de deteção explícita e decisão humana.
Que acontece se o mesmo pedido for enviado duas vezes por engano, sem chave de idempotência?
O servidor processa a operação duas vezes — o cenário mais comum é duplicação de baixas de stock ou notificações. Com uma chave de idempotência gerada na origem e reivindicada atomicamente no servidor, o segundo pedido é reconhecido como duplicado e devolve a resposta já processada.
Fontes
- SatSig — Geostationary satellite latency and time delay
- Offshore Magazine — For oil and gas satcom users: How much bandwidth is enough?
- Wikipédia — Starlink
- crdt.tech — About CRDTs
- DZone — Conflict Resolution: Using Last-Write-Wins vs. CRDTs
- AWS Builders' Library — Making retries safe with idempotent APIs