← Todas las utilidades

NSAdaSync

Versión 26.1 (build 214)
NSAdaSync
Índice

Qué es NSAdaSync

nsadasync (NSAdaSync) es la réplica de Adabas de Nacarsoft: mantiene ficheros Adabas (Open Systems) sincronizados con Oracle, PostgreSQL, MySQL, MariaDB, SQLite, MSSQL, MongoDB, Elasticsearch y Kafka. Los cambios salen del núcleo con un user exit (ADAUEX_4), un supervisor interno los lee del pipe FIFO y los publica en el broker nsmq embebido (en proceso, sin TCP), y los consumidores aplican cada suscripción (fichero → tabla/colección/tópico/índice) en el destino, con carga inicial, checkpoints, DLQ con replay y auditoría completas.

núcleo Adabas --(exit UEX4, frames NSQ2)--> pipe FIFO --(supervisor interno)--> broker embebido
                                                                                         |
                                         +---------------+---------------+---------------+
                                         |               |               |               |
                                      Oracle/PG/       MongoDB /      Kafka (topics   SQLite
                                      MySQL/MSSQL    Elasticsearch    + tombstones)   (WAL)

Suscripciones de NSAdaSync: ficheros del origen hacia tablas destino

Presentación comercial: PDF (ES) · PPTX (ES) · PDF (EN) · PPTX (EN)

Los nombres de bases, ficheros, tablas y destinos de los ejemplos son ficticios.

Fan-out de NSAdaSync: un origen Adabas replica a Oracle, PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, MongoDB, Elasticsearch y Kafka

Panel de destinos de NSAdaSync: los nueve motores con su estado, tipo y número de suscripciones

Características principales

  • Captura mínima en el núcleo: exit UEX4 mínimo (un writev no bloqueante por registro; nunca bloquea, reintenta ni toca red) + FIFO + lector por origen supervisado en proceso + broker persistente con fsync por batch.
  • Nueve destinos: Oracle, PostgreSQL, MySQL, MariaDB, SQLite, MSSQL, MongoDB, Elasticsearch y Kafka, con ping/test, inspección de tablas/filas y DDL generado. Sin clientes nativos: drivers integrados.
  • Suscripciones fichero → tabla: mapeo de campos del DDM (JSON), MU/PE como JSON o tablas hijas, identidad por ISN y upsert con guarda de recencia _seq (los eventos tardíos no pisan a los nuevos).
  • Columnas calculadas: campos de marca temporal (created_at en el insert, updated_at en insert/update) que se rellenan solos al aplicar, y campos Adabas añadidos a mano con sus datos de destino; el botón Aplicar al destino alinea las tablas sin tocar datos.
  • Carga inicial + streaming: modos merge/empty/drop, capacidad por destino (max_loads), checkpoints (high_water_seq) y reanudación del backlog tras crear la tabla. La carga valida antes destino accesible y origen activo (409 si alguno está parado).
  • Panel de destinos y orígenes: estado, latencia, suscripciones y registros encolados por punto, con detalle en tooltip.
  • Destino caído = encolado, no error: si un destino no responde, los cambios esperan en el broker y reanudan solos al volver, sin ensuciar la DLQ.
  • Comparación origen ↔ destino por ISN (contadores + diffs por campo), bajo demanda y cancelable; registros de origen y destino en el mismo visor.
  • DLQ con replay: los errores no pierden eventos, se inspeccionan y se reintentan; el replay rechaza campos sin mapeo en vez de corromper (los borrados se reintentan por ISN, sin decodificar imagen).
  • Historial de cargas con borrado individual o total y antigüedad (hace 20h:30m) por ejecución.
  • Kafka como sink de streaming: un tópico por tabla, clave = ISN, tombstones al borrar (converge con compactación), idempotencia y SASL/TLS.
  • Consola web en español e inglés: Panel, Orígenes, Suscripciones, Destinos, Usuarios, Roles, Logs y Config.
  • Tokens máquina (nstm_...) con scope por suscripción, caducidad y allowlist de CIDRs (API máquina actualmente deshabilitada; el tráfico interno no usa tokens).
  • RBAC (15 permisos, roles Admin/SyncOperators/Viewers), auditoría de cada acción.

Casos de uso

  • Analítica y BI: replicar a Oracle, PostgreSQL, MySQL o MariaDB y alimentar el reporting en directo, sin cargas nocturnas ni ficheros intermedios.
  • Streaming de eventos: publicar en Kafka con un tópico por tabla, clave ISN y tombstone al borrar: converge con la compactación y es idempotente.
  • Búsqueda y texto: mantener un índice por tabla en Elasticsearch, creado durante la carga inicial y alimentado en vivo.
  • Aplicaciones modernas: MongoDB y SQL Server reciben documentos y filas que alimentan APIs y aplicaciones sin tocar el núcleo.
  • Varios destinos a la vez: la misma suscripción se duplica sobre cada destino (fan-out) o se crean suscripciones distintas por fichero.
  • Consolidación: el Adabas Open Systems pasa a ser la fuente y SQL, NoSQL o las plataformas de streaming pasan a ser consumidores, no al revés.

Requisitos

  • Linux (RHEL 8/9) con los binarios precompilados (sin dependencias de compilación).
  • Adabas Open Systems con LOGGING (CLOG) y LOGRB, USEREXITS=4 y ADAUEX_4=<ruta>/libnsadasync.so (compilado con exits/build.sh). Importante: debe ser un LOGGING = (CB) efectivo — una línea LOGGING = vacía posterior en el INI lo anula en silencio y la exit no recibe ningún cambio (síntoma: todo replica por cargas pero el streaming no avanza).
  • Diccionario por origen: JSON de nsddm (misma semántica que nsadabrowse); hasta 8 MiB por fichero.
  • adashow/adarep en el PATH para la sonda de orígenes (el servidor añade /opt/softwareag/Adabas{,/bin} solo).
  • Proxy inverso con HTTPS en producción (consola :11035; el broker es embebido y no escucha TCP).

Arranque

# Inicializar (crea config.yaml 0600, db 0600 y usuario admin) y arrancar
./nsadasync init ./data
./nsadasync start ./data
./nsadasync status ./data
./nsadasync stop ./data

Consola web en http://host:11035/ (login admin; cambia la contraseña tras el primer acceso). Comandos útiles: nsadasync passwd [dir] <usuario> [password] (recuperar acceso) y nsadasync license (estado, tipo, caducidad y fingerprint).

Arquitectura

Arquitectura de NSAdaSync: núcleo Adabas, exit UEX4, pipe FIFO, broker nsmq embebido y worker por suscripción

Tres piezas entre el núcleo y el destino, cada una con su observabilidad:

  1. Exit del núcleo (exits/libnsadasync.so, ADAUEX_4). Por registro emite un frame binario (cabecera NSQ2 de 76 B + CL/FB/RB crudos; se acepta el legado NSQ1) con un único writev no bloqueante. Captura todo salvo los FNR de NSADASYNC_EXIT_NOFILES; el filtrado por suscripción ocurre aguas abajo (lo no suscrito se descarta antes de publicar, nunca toca disco). Sin pipe explícito queda inerte (fail-closed). Ante cortes vuelca al journal de outage (<pipe>.journal, tope 1 GB) que el supervisor reinyecta. La versión del exit (de exits/VERSION + exits/BUILD) sale en la traza de init. Los borrados viajan sin after-image (fb vacío): se aplican por ISN sin decodificar.
  2. Supervisor de forward (en el servidor, sin proceso ni token aparte). Mantiene un lector del FIFO por cada origen con pipe_path (reconcilia cada 30 s), con spool por origen y relectura del journal tras cortes. Publica en el broker por tópicos numéricos adasync.<dbid>.<FNR>: el rendez-vous pipe/journal sale de la fila Origins.
  3. Broker + consumidores (en el servidor). El broker nsmq persiste un log segmentado por tópico en data_dir con fsync por batch y aplica retención (retention_bytes 10 GB / retention_hours 72 h). El consumer manager mantiene un worker por suscripción activa (reconcilia cada 30 s y tras cada alta/baja desde la UI), aplica con el adapter del destino y avanza el checkpoint high_water_seq. Si el destino no responde, el worker aparca en el broker con backoff (sin avanzar checkpoint, sin DLQ) y reanuda al volver; si la tabla no existe, aparca igual hasta que se crea. Cada suscripción tiene su propio log (logs/subs/<id>.log, rotación a 10 MB) y su contador de errores; con debug registra cada apply.

Configuración

Orígenes (Adabas fuente)

Cada origen vive en la página Orígenes (SQLite), no en config.yaml: name (debe ser el DBID numérico, p. ej. 50: los tópicos son siempre adasync.<dbid>.<FNR>), dbid, config_file (JSON nsddm), connect, adabas_host/adabas_port (inspección en vivo; vacío = localhost, puerto 0 = puerto TCP de adashow) y pipe_path/journal_path (rendez-vous FIFO + journal de cortes; journal vacío deriva a <pipe>.journal; el pipe debe ser ruta absoluta). Un origen sin pipe no tiene streaming (solo cargas e inspección).

La consola valida el diccionario al adjuntarlo (POST /api/adabas/:db/dictionary, hasta 8 MiB, se guarda en data/ddm/<db>.json), lo recarga (POST .../reload), inspecciona ficheros (GET /api/adabas/:db/files), campos aplanados con nombres largos (GET .../files/:fnr, con rutas con puntos DV.DW para PE/MU), el DDM fuente (GET .../raw?ddm=NOMBRE), detecta ficheros sin suscripción (GET .../missing-subscriptions, con conteos en vivo) y sondea el núcleo (GET .../probe, POST .../validate, vía adaprobe + adashow/adarep).

Borrar un origen (DELETE, permiso config.manage) es en cascada: sus suscripciones (y tablas destino si el destino tiene drop_on_delete), su DLQ, su historial de cargas y sus topics del broker.

Exit del núcleo

# En el host del núcleo
./build.sh                      # en exits/: genera libnsadasync.so
ADAUEX_4=<ruta>/libnsadasync.so # + USEREXITS=4, LOGGING=(CB) efectivo, LOGRB

Variables del exit: NSADASYNC_EXIT_PIPE (obligatoria), NSADASYNC_EXIT_NOFILES="(1,2,3)" (FNRs a saltar), NSADASYNC_EXIT_JOURNAL (defecto <pipe>.journal) y NSADASYNC_EXIT_DEBUG=1|2 + NSADASYNC_EXIT_DEBUG_FILE (traza 0600 con tope de 64 MB; el nivel 2 vuelca FB/RB truncados; la marca de arranque muestra los valores efectivos). Sin pipe el exit queda inerte y lo dice una sola vez en la traza.

Destinos

KindConexiónNotas
sqlitepath al fichero (se crea en WAL al darlo de alta)DDL + inspección completas; max_loads 1 por defecto (serializa escritores)
postgres/postgresqlDSN postgres://user:pass@host:5432/dbupsert nativo + guarda _seq; los NUL se eliminan solo en texto (en BYTEA se conservan)
mysql/mariadbDSN user:pass@tcp(host:3306)/dbupsert nativo + guarda _seq
mssql/sqlserverDSN sqlserver://user:pass@host:1433?database=dbupsert + guarda _seq
oracleDSN oracle://user:pass@host:1521/SIDSin cliente nativo (go-ora)
mongodb/mongoDSN mongodb://user:pass@host:27017/db (db obligatoria)Documentos con grupos anidados nativos; colecciones se crean con la carga
elasticsearchDSN http(s)://host:9200[/prefijo]Índices por tabla (se crean con la carga); tipos por campo del mapping
kafkaDSN kafka://h1:9092,h2:9092[/prefijo][?sasl=…&user=…&pass=…&tls=1]Solo streaming: sin cargas iniciales ni lecturas; tópico por tabla creado al activar; tombstone al borrar; el borrado conserva el tópico

Comunes a todos: pe_mode (json|child, defecto json) para los grupos periódicos, mu_mode (json|flat|child, defecto json) para los campos múltiples, isn_named (ISN_<TABLA> en vez de isn), groups_as_json (colapsa grupos PE/MU a una columna JSON en vez de aplanarlos), skip_supersub (defecto true: oculta superdescriptores y sus componentes), drop_on_delete (al borrar la suscripción, borra padre + hijas), max_loads (cargas concurrentes por destino), max_records (0 = sin tope, limita las cargas), options (JSON libre). GET /api/destinations/drivers dice qué driver trae el build; usa ping (destinations.view) y test (destinations.manage) antes de replicar, e inspecciona tablas y filas (GET .../tables[/:table/rows]; columnas tipadas en SQL y documentos; en colecciones anchas se listan hasta 2048 columnas).

Destinos de NSAdaSync: los nueve kinds con ping e inspección

Consola (/config)

Panel por secciones (Server, Correo, License, MQ, Exec…) con guardado en config.yaml (0600); al guardar indica qué áreas piden reinicio. Botón Test Email. Secretos también por entorno: NSADASYNC_SMTP_PASSWORD y NSADASYNC_TOKEN_PEPPER (cambiar el pepper invalida todos los tokens). Referencia interactiva en /api/docs + /api/openapi.yaml (apiDocs.access: public|private|off, defecto private).

SecciónClaves principales (defectos)
serverhost 0.0.0.0, port 11035, trustedProxies [127.0.0.1, ::1]
authjwtSecret (mín. 32), tokenPepper, jwtExpiryMin 15, cookieName session, secureCookies auto
mqEmbebido sin TCP; data_dir ./mq-data, retention_bytes 10 GB, retention_hours 72, batch_size 200, batch_timeout_ms 100
sync.defaultsmu_pe json, conflict upsert
correomailer sendmail|smtp, host/puerto/seguridad/usuario/password/from, sendmailPath, baseUrl (URL pública de los enlaces)
execworkDir, timeouts 60/300 s, maxConcurrent 10, topes 1 MB, ratePerMin 600, queuetimeout 20, callRetentionDays 30 (purga diaria de audit/DLQ; 0 = conservar)
logging/license/updatesRotación 10 MB × 10, 30 días; trialDays 30; URL de updates

Uso

Panel de NSAdaSync: orígenes, destinos, suscripciones y salud de la cadena

El Panel muestra juntos cargas en curso, destinos y su estado (accesibilidad, latencia, suscripciones y encolados, con detalle en tooltip) y orígenes y su estado (actividad del núcleo y latido del forwarder), además de estados por suscripción, totales I/U/D, gráfica de cambios, encolados por suscripción, salud de la cadena y consistencia.

Suscripciones

Una suscripción = un fichero (origen, FNR, DDM) → (destino, tabla) con mapeo de campos, pe_mode, mu_mode, mu_flat_max, ddl_override, is_active y debug. El nombre es único (se sufija por destino) y la pareja (destino, tabla) no se puede reclamar dos veces. filter_expr se guarda con la suscripción (reservado). pe_mode y mu_mode vacíos heredan los del destino; mu_flat_max es obligatorio si mu_mode=flat.

El mapeo es un JSON de columnas [{adabas, column, mode?, type?, length?, precision?, index?, func?}] donde adabas es el nombre corto del DDM; sin entrada se hereda el tipo del diccionario y la columna por defecto (nombre largo saneado, corto si falta; en mongo/ES/kafka, la ruta en minúsculas). index: false desindexa aunque el DDM marque descriptor. Flujo recomendado: Prepare (POST .../prepare, valida FDT frente al diccionario) → Preview DDL (POST .../preview-ddl, sin ejecutar) → Ensure table (POST .../ensure-table, crea padre + hijas + índices y guarda el último error DDL en la fila) → Load → streaming.

Campos personalizados (botón + en la tabla de campos): abre un modal para añadir un campo Adabas (elige de la lista de pendientes con nivel, corto, formato y longitud; defines columna, tipo, longitud, precisión, modo e índice) o una columna calculada (created_at: sella el momento del insert; updated_at: insert y update). Las calculadas van siempre al final y existen en DDL, cargas, streaming y replays. Tras editar, Aplicar al destino (POST .../align-columns) añade las columnas que falten sin tocar datos, contadores ni checkpoint (409 si hay carga en curso o la tabla aún no existe).

Duplicate (POST .../duplicate) clona la suscripción sobre otro destino (fan-out, hereda filtro y modos); Compare (POST/GET .../compare, cancelable, 409 si ya corre uno) recorre por ISN y devuelve contadores (read/total/outdated/missing/extra) + diffs por campo; los registros de origen se ven en el mismo panel que los de destino (tabla/formulario, búsqueda por ISN, paginación); debug hace que el worker registre cada apply (los errores, siempre) en logs/subs/<id>.log; reset-counters pone a cero contadores; el log por suscripción (GET .../log?lines=200, máx. 1000; DELETE lo limpia).

EstadoSignificado
okActiva, tabla existe, conteos origen/destino coinciden
diferenteSolo informativo: el destino va con retardo transitorio; el contenido se verifica con compare
sin_tablaActiva sin tabla: el consumidor aparca en el broker sin ensuciar la DLQ; crear la tabla reanuda el backlog (en mongo/ES la crea la carga; en kafka el tópico se crea al activar)
cargando / en_colaUna carga inicial posee las tablas (o espera capacidad); live/replay se aparcan
errorError del consumidor o DLQ pendiente (ver motivo)
inactivais_active=false (interruptor ON/OFF)
desconocidoOrigen inalcanzable para contar o destino no verificable

Panel de suscripciones de NSAdaSync: estado de cada suscripción, carga inicial, contadores I/U/D y SEQ de control

Cargas iniciales, journal y DLQ

Loads (POST /api/loads {subscription_id, mode}, permiso loads.run; POST /api/loads/batch para varias; GET /api/loads[/:id], POST .../cancel, DELETE .../:id y DELETE ...?subscription_id= para borrar historial terminado —409 si hay carga activa—): modos merge (defecto), empty y drop (estos dos exigen destino SQL/mongo/ES; kafka rechaza la carga con 501: es solo streaming, basta activarla). Antes de encolar, la carga exige destino accesible y origen activo (409 si alguno está parado) y tablas listas (409 en vez de morir a mitad). La capacidad la marca max_loads por destino (las demás esperan en cola); al reiniciar, las interrumpidas se aparcan como canceladas y las encoladas se reanudan solas (Resume). Cada ejecución muestra su antigüedad (hace 20h:30m).

Journal (GET /api/journal/status, journal.read): checkpoints high_water_seq por suscripción. System/Health (GET /api/system, /api/health, más el agregado GET /api/health/endpoints con destinos y orígenes, suscripciones y backlog): salud global con accesibilidad de destinos. MQ (GET /api/mq/status): tópicos adasync.<dbid>.<FNR> con offsets y checkpoints; inspección (GET .../topics/:topic/messages), purga (POST .../purge {mode: all|to_offset, offset}, mq.manage) y borrado (DELETE, mq.manage). La retención del broker + la purga diaria de audit/DLQ (callRetentionDays) acotan el disco.

DLQ (GET /api/dlq, dlq.view): entradas con error; replay (POST /api/dlq/:id/replay) y borrado (DELETE, dlq.replay). El replay rechaza campos sin mapeo (unmapped fields… check catalog) en vez de corromper el destino; los borrados se reintentan por ISN sin decodificar imagen.

Cambios de estructura: no hace falta carga inicial

La carga inicial es solo el backfill: una vez que la suscripción está en streaming, el esquema puede evolucionar sin volver a cargar. Los tres escenarios habituales:

EscenarioQué hacer¿Carga inicial?
Cambia el Adabas (campo nuevo en el fichero, DDM actualizado)Recargar el diccionario (POST /api/adabas/:db/reload), añadir el campo al mapeo de la suscripción y pulsar Aplicar al destino (POST .../align-columns): añade la columna a la tabla existente sin tocar datos, contadores ni checkpointNo
Cambia el destino (columna nueva, tipo corregido)El upsert respeta la columna; si el tipo no encaja, se corrige con Aplicar al destino o con DDL manualNo
Se borra la tabla del destinoLa suscripción pasa a sin_tabla y el worker se aparca en el broker (no es un error ni llena la DLQ); al recrearla (Preparar → Crear tabla) reanuda el backlogNo

Si llega un campo que no está en el diccionario ni en el mapeo, el apply se rechaza con el detalle unmapped fields… check catalog y la entrada va a la DLQ: se corrige el mapeo y se hace replay — tampoco hace falta cargar.

Editor de suscripción de NSAdaSync: validación de campos, preview de DDL, creación de tabla y aplicación al destino

Nota: align-columns solo aplica a destinos SQL (501 en mongo, ES y kafka: allí los documentos, índices y tópicos se materializan con la propia escritura).

Grupos PE y campos múltiples MU

El tratamiento de los grupos y el de los campos múltiples se eligen por separado, en el destino y en cada suscripción (la suscripción manda sobre el destino; vacío = heredar):

SelectorValoresAfecta a
Grupos (PE)json | childgrupos periódicos y grupos con subcampos
Campos múltiples (MU)json | flat | childcampos con múltiples ocurrencias (MESES, DES-BENEFICIARIO…)
Máx. ocurrencias1…999 (solo suscripción)obligatorio si el modo MU es flat
  • Grupos json: el grupo periódico entero viaja como un documento JSON (arrays nativos en MongoDB/ES/Kafka). child: tablas hijas normalizadas, resueltas igual en el DDL, las cargas y los applies.
  • Campos MU json: todas las ocurrencias en una columna JSON. child: una fila por ocurrencia en una tabla hija (columna VALUE). flat: las ocurrencias se despliegan en columnas numeradas de la misma tabla: si MESES tiene 8 ocurrencias y el modo es flat con máximo 8, la tabla lleva MESES_1, MESES_2 … MESES_8.
-- Destino SQL con un campo MU de 8 ocurrencias en modo flat (max 8)
CREATE TABLE "CLIENTES" (
  "ISN_CLIENTES" INTEGER PRIMARY KEY,
  "MESES_1" TEXT, "MESES_2" TEXT, "MESES_3" TEXT, "MESES_4" TEXT,
  "MESES_5" TEXT, "MESES_6" TEXT, "MESES_7" TEXT, "MESES_8" TEXT,
  ...
);

Detalles del modo flat de un campo MU:

  • El número de ocurrencias se indica en la suscripción (mu_flat_max, 1…999) y es obligatorio cuando el modo es flat; sin él la API responde 400.
  • Al aplicar un registro, la 1.ª ocurrencia va a MESES_1, la 2.ª a MESES_2, etc.; las que falten quedan a NULL.
  • Si el registro trae más ocurrencias que el máximo, se conservan las primeras y las demás se descartan con un aviso en el log de la suscripción (logs/subs/<id>.log): la réplica sigue, no se rompe el flujo.
  • Sirve igual en SQL y en documentos (MongoDB, Elasticsearch, Kafka): allí el campo se guarda con los mismos nombres numerados.
  • El DDL se genera con los nombres numerados, y Aplicar al destino (align-columns) añade las columnas que falten sin tocar datos, contadores ni checkpoint.
  • El compare origen ↔ destino compara cada ocurrencia con su columna.

El modo explícito de un campo en el mapeo (columna «Modo») manda sobre el selector: un campo MU puede marcarse como flat aunque el destino use json. Con groups_as_json el destino colapsa además los grupos simples a una columna.

La identidad es isn (o ISN_<TABLA> con isn_named) y el conflicto se resuelve con upsert + guarda _seq (un evento tardío nunca pisa a uno nuevo).

Línea de comandos

nsadasync init ./prod                        # proyecto (config 0600, db, admin)
nsadasync token ./prod --name poller --scope VENTAS --days 365 --cidr 10.0.0.0/8
nsadasync token ./prod --list                # prefijo, nombre, scope, caducidad, revocados, último uso
nsadasync token ./prod --revoke nstm_a1b2c3d4
nsadasync passwd ./prod admin [nuevapass]    # emergencia (mín. 8 caracteres)
nsadasync start|stop|status ./prod [--port N]
nsadasync license                            # estado, tipo, caducidad, fingerprint

Notificaciones por correo

NSAdaSync puede avisar por correo cuando algo cambia en la réplica. El motor evalúa el estado (destinos, orígenes, suscripciones, DLQ, cargas y licencia) y avisa solo cuando algo cambia, no en cada comprobación; si el problema persiste, lo recuerda cada reminder (4 h por defecto) y avisa de la recuperación a los mismos destinatarios que el aviso original.

Se activa y ajusta en la consola (Configuración → Notificaciones) o en config.yaml (sección notify):

notify:
  enabled: true
  methods: email
  recipients:
    critical: ["dba@empresa.com"]
    warning: ["equipo-datos@empresa.com"]
    info: []
  intervalSec: 60
  reminder: 4h
  quiet: { enabled: false, from: "22:00", to: "07:00" }
  rules:
    destination_down: { enabled: true, severity: critical }
    lag_high: { enabled: true, severity: warning, threshold: 1000 }
SucesoSeveridadCuándo salta
destination_downcríticaun destino no responde (los cambios se encolan)
origin_stoppedcríticaun origen está parado con suscripciones activas
dlq_pendingcríticahay cambios sin aplicar en la DLQ
load_failedcríticauna carga inicial termina en error
subscription_erroravisouna suscripción queda en error
table_missingavisofalta la tabla en el destino (backlog esperando)
lag_highavisomás cambios encolados que el umbral
destination_slowavisolatencia del destino por encima del umbral
broker_diskavisoel broker de la cola cerca del límite de disco
license_expiringavisola licencia caduca en pocos días
login_failuresavisovarios intentos de acceso fallidos seguidos
destination_recoveredinfoun destino vuelve (se aplica el backlog)
load_okinfouna carga inicial termina bien
subscription_changeinfoalta o cambio de una suscripción

Desde el CLI: nsadasync notify test [destinatario] envía un aviso de prueba y nsadasync notify status muestra las reglas, las incidencias abiertas y los últimos avisos enviados. El historial queda registrado y consultable en la consola y en GET /api/notifications.

Programación

NSAdaSync lleva su propio motor de programación (no usa el cron del sistema): cada 30 s evalúa las reglas y dispara las que tocan. En la consola está en Programación (menú lateral) y el Panel muestra cuántas reglas hay programadas y cuál es la próxima ejecución.

Ventana de mantenimiento: pausa la replicación de un origen, un destino o de todos entre dos horas (recurrencia diaria, semanal, mensual o una vez). Durante la ventana los cambios se encolan en el broker y se aplican al terminarla. Al abrirla se guarda el estado previo de cada endpoint, así que:

  • lo que ya estaba parado a mano no se toca;
  • al cerrarla se reanuda solo lo que la ventana paró;
  • la consola distingue «parado a mano» de «pausado por ventana».

Tareas programadas: una carga inicial (merge/empty/drop) o una comparación origen↔destino de una suscripción, con hora y recurrencia. Si ya hay una carga en curso, la tarea se omite y se avisa por email; con catch_up se ejecuta aunque la hora haya pasado mientras el servidor estaba parado.

Lo voluminoso no se guarda en la base de datos: el detalle de una comparación (hasta 200 diferencias con valores) se escribe en un fichero JSON del directorio de datos (compares/*.json); en la base queda el resumen (contadores + extracto de 10 diferencias + la ruta del fichero) y el historial de ejecuciones, acotado.

Cada aviso por email (apertura y cierre de ventana, tarea omitida, error, carga terminada y resultado de la comparación) lleva la fecha y hora del suceso.

GET  /api/schedules · /api/schedules/summary · /api/schedules/runs
GET  /api/schedules/compares   (resúmenes de las comparaciones)
POST /api/schedules · POST /api/schedules/:id/run
PUT/DELETE /api/schedules/:id

El historial está acotado y se purga con la retención habitual (30 días por defecto): los avisos, las ejecuciones (las 20 últimas por regla), los resúmenes de comparación y los ficheros de detalle compares/*.json.

Desde el CLI: nsadasync schedule list y nsadasync schedule rm <id>.

Usuarios, grupos y permisos

RBAC de un solo tenant: roles de sistema Admin (todo), SyncOperators (operar suscripciones, destinos, journal, loads y DLQ: dashboard.view, subscriptions.view, destinations.view, journal.read, loads.run, dlq.view, dlq.replay, calls.view, logs.view) y Viewers (solo lectura); grupos Administrators, Operators, Readers. El catálogo se refresca en cada arranque. Usuarios (GET/POST /api/users, PUT/DELETE /api/users/:id, PUT .../active, PUT .../groups, todo con users.manage); roles y grupos solo se listan (GET /api/roles, /api/groups). Recuperación por email (/forgot-password, POST /api/auth/forgot-password, POST .../reset-password) si el correo está configurado; si no, nsadasync passwd.

PermisoCubre
dashboard.viewPanel y estados
subscriptions view|manageVer / crear, editar, borrar, ensure-table, align-columns, duplicar, debug
destinations view|manageVer, ping / crear, editar, borrar, test
journal.readJournal, system, health (+endpoints), MQ (lectura)
mq.managePurga y borrado de tópicos
loads.runLanzar, cancelar y borrar cargas
dlq view|replayVer / reintentar y borrar entradas
calls.view, logs.viewLlamadas y auditoría/logs
tokens view|manageVer / crear, revocar y borrar tokens
users.manageUsuarios, roles y grupos
config.manageConfiguración, email de prueba, orígenes y diccionarios

Los tokens máquina (nstm_..., solo se muestran una vez) llevan scope de suscripción o all, caducidad (--days, 0 = nunca) y allowlist de CIDRs/IPs; se guardan como HMAC-SHA256 con el pepper (rotarlo los invalida todos). La API máquina (/api/ext) está deshabilitada en esta versión: todo el tráfico es consola local o loopback.

POST /api/auth/login · /api/auth/refresh · POST /api/auth/logout
POST /api/auth/forgot-password · POST /api/auth/reset-password
GET  /api/auth/me · POST /api/auth/{theme,language}
GET  /api/version · /api/license · /api/docs · /api/openapi.yaml
GET  /api/subscriptions · /api/subscriptions/states · /api/subscriptions/check-name
POST /api/subscriptions · PUT/DELETE /api/subscriptions/:id
GET  /api/subscriptions/:id/log · DELETE .../log
POST /api/subscriptions/:id/{debug,reset-counters,ensure-table,align-columns,prepare,duplicate}
POST /api/subscriptions/{preview-ddl,validate-fields}
POST /api/subscriptions/:id/compare · GET .../compare · POST .../compare/cancel
GET  /api/subscriptions/:id/origin-rows
GET  /api/adabas/databases · GET /api/adabas/:db/files[/:fnr[/raw]]
GET  /api/adabas/:db/missing-subscriptions · GET /api/mapping-template
PUT/DELETE /api/adabas/databases/:db · POST /api/adabas/:db/{dictionary,reload}
GET  /api/adabas/:db/probe · POST /api/adabas/:db/validate
GET  /api/destinations · /api/destinations/drivers
POST /api/destinations · PUT/DELETE /api/destinations/:id
POST /api/destinations/:id/ping · POST /api/destinations/test
GET  /api/destinations/:id/tables[/:table/rows]
POST /api/loads[/batch] · GET /api/loads[/:id] · POST /api/loads/:id/cancel
DELETE /api/loads/:id · DELETE /api/loads?subscription_id=
GET  /api/journal/status · /api/system · /api/health · /api/health/endpoints
GET  /api/mq/status · GET /api/mq/topics/:topic/messages
POST /api/mq/topics/:topic/purge · DELETE /api/mq/topics/:topic
GET  /api/dlq · POST /api/dlq/:id/replay · DELETE /api/dlq/:id
GET  /api/users · /api/roles · /api/groups · /api/tokens · /api/audit
GET  /api/config · PUT /api/config · GET /api/config/disk · POST /api/config/test-email

Todo queda en auditoría (LOGIN/LOGOUT/LOGIN_FAILED, SUBSCRIPTION_*, DESTINATION_*, ORIGIN_DELETE, TABLE_CREATE/TABLE_DROP*/TABLE_ALIGN, LOAD_*/LOADS_DELETE, PREPARE, MQ_TOPIC_*, DLQ_*, TOKEN_*, USER_*, CONFIG_SAVE, EMAIL_TEST, PASSWORD_*, con IP de cliente). La sesión dura jwtExpiryMin (defecto 15 min); al caducar, la consola vuelve al login.

Seguridad

  • JWT HS256 con caducidad configurable (defecto 15 min; sin refresco silencioso: al expirar se pide login), anti-bruteforce en login/reset, contraseñas con bcrypt (coste 12) y reseteo con tokens de un solo uso.
  • config.yaml (0600) y DB (0700); secretos redactados en GET /api/config y nunca en logs; tokens máquina por hash HMAC + prefijo visible, con CIDR allowlist y expiración.
  • server.trustedProxies (defecto loopback) para no falsificar la IP de auditoría/rate-limit; cookies Secure en auto (TLS/X-Forwarded-Proto); cabeceras Content-Security-Policy, X-Content-Type-Options, Referrer-Policy y Permissions-Policy.
  • Validación y saneado de DBIDs, FNRs, nombres de suscripción/tabla, DDL, DSNs y rutas (config_file, path, pipe/journal); los topics son siempre numéricos y el agente remoto no existe aquí (todo es local).

Estado y control de la instalación

  • Arranque y parada: nsadasync init|start|stop|status <dir>; el proceso arranca el supervisor de forward, el broker embebido y los workers de las suscripciones activas, y los detiene de forma ordenada al parar.
  • Estado: nsadasync status <dir> y el Panel de la consola (orígenes, destinos, suscripciones, encolados, DLQ, salud de la cadena).
  • Reanudación: al reiniciar, las cargas interrumpidas quedan canceladas y las encoladas se reanudan solas; el backlog del broker se reaplica en orden y cada suscripción continúa desde su checkpoint.
  • Operación por CLI: nsadasync passwd <dir> <usuario> [password] (recuperar acceso), nsadasync token <dir> --name … --scope … --days … (tokens de máquina) y nsadasync license (estado de la instalación).
  • Auditoría: todas las acciones quedan registradas en la consola (LOGIN, SUBSCRIPTION_*, DESTINATION_*, LOAD_*, DLQ_*, TOKEN_*, USER_*, CONFIG_SAVE…) con usuario e IP de cliente, y en el log de cada suscripción (logs/subs/<id>.log).

Solución de problemas

  • El exit no emite (streaming parado pero las cargas funcionan): además de ADAUEX_4, USEREXITS=4 y CLOG+LOGRB, verifica que el LOGGING=(CB) sea efectivo: una línea LOGGING = vacía posterior en el INI lo anula en silencio (no sale en el PARSET) y la exit no recibe ningún cambio. Con NSADASYNC_EXIT_DEBUG_FILE obtienes la traza de init y cada evento pipe/journal.
  • adaopr db=<id> disp=hi aborta con Signal 8: es un bug de Software AG (div por cero en dis_hwm de libadautl.so al formatear high-water marks), no de la exit: el núcleo sigue vivo. Evita ese display.
  • El forward no publica: comprueba el pipe_path/journal_path de la fila Origins, el spool por origen y que el supervisor lea el FIFO (log [FORWARD]); sin lector, la exit vuelca al journal y el supervisor lo reinyecta al volver.
  • Destino caído: los cambios se encolan en el broker (verdad en el panel de destinos: encolados por punto) y reanudan solos; nada va a la DLQ. Revisa con ping/test, GET /api/health/endpoints y GET /api/destinations/drivers (el build puede no traer ese driver). Las cargas iniciales se niegan con 409 hasta que el destino responde.
  • Origen parado: la carga inicial se niega con 409; el streaming simplemente no recibe más cambios (el backlog se conserva).
  • Suscripción sin_tabla: estacionamiento, no error — crea la tabla (ensure-table) y reanuda el backlog del broker (en kafka, activa: el tópico se crea solo).
  • diferente permanente: normal en vivo (retardo transitorio); si el contenido diverge, lanza un compare por ISN.
  • DLQ con unmapped fields: el DDM no cubre esos campos — amplía el diccionario o el mapeo y haz replay.
  • Carga atascada en cargando/en_cola: mira GET /api/loads/:id (capacidad max_loads por destino); cancélala y relánzala; al reiniciar, las interrumpidas se aparcan solas. El historial se borra por entrada o entero (DELETE /api/loads[/:id]).
  • Editaste el mapping y faltan columnas: usa Aplicar al destino (solo añade columnas, sin resetear nada).
  • Compare con 409: ya corre uno en esa suscripción (un compare por vez); cancélalo o espera.
  • Token rechazado: revisa scope, CIDR y caducidad; si rotaste el pepper, reemite todos los tokens. La API máquina está deshabilitada en esta versión.
  • Sin ficheros en el origen: revisa config_file (JSON nsddm válido, ≤ 8 MiB) y connect; usa probe/validate del origen.
  • Sesión caducada: vuelve al login (sin refresco silencioso); ajusta jwtExpiryMin si necesitas sesiones largas.
  • Puerto ocupado: cambia server.port (11035) en config.yaml.

También disponible en: en