Pruebas de carga (stress test)¶
Un harness local para exprimir el gateway sin pegarle a AFIP homologación: no emite CAE reales. El foco son las tres piezas que deciden si el sistema aguanta la flota real de POS:
- El
NumeracionLock(sp_getapplockde SQL Server) — la serialización de la numeración por punto de venta. - El pool Hikari — cuántas conexiones JDBC hacen falta con N POS emitiendo a la vez.
- La correctitud del dedup bajo carrera — el objetivo no negociable: 0 doble-CAE.
Todo el harness es JDK puro, sin dependencias (MockArcaServer.java + StressDriver.java), y vive en la carpeta stress/ del repo. El detalle operativo completo (comandos, keystore, seed) está en stress/README.md.
Por qué un simulador ARCA propio
Pegarle a AFIP homologación para una prueba de carga no sirve: emitiría comprobantes reales y su latencia (~800 ms) taparía lo que se quiere medir (el gateway y la DB). El MockArcaServer responde WSAA LoginCms + WSFE (FECAESolicitar / FECompUltimoAutorizado / FEDummy) con latencia configurable, así se puede medir throughput puro (latencia 0) o simular AFIP real (latencia 800 ms).
Las piezas¶
Archivo (stress/) |
Qué es |
|---|---|
MockArcaServer.java |
Simulador ARCA HTTPS. Siempre aprueba (no valida números ni Auth): mide el gateway + DB, no a AFIP. |
StressDriver.java |
Driver de carga concurrente (V1/V2). Mide throughput y latencias p50/p95/p99 + errores. |
application-stress.yml |
Perfil del gateway para la corrida: AFIP → mock, SQL Server local, Hikari dimensionado a la flota, jobs de fondo apagados. |
seed-ta.sql |
Siembra un TA falso vigente → el gateway no hace LoginCms (no depende del .p12). |
cleanup.sql |
Borra las Trx del stress tras la corrida. |
Cómo se corre¶
Requisitos: JDK 17, SQL Server local con la DB tifacturaonline (con las migraciones aplicadas), y el ente del stress habilitado. Se levantan 4 procesos (ver stress/README.md para el paso a paso con el keystore):
# 1) Simulador ARCA (arg 4 = latencia AFIP en ms: 0 = throughput puro, 800 = AFIP real)
java -cp stress/out MockArcaServer 8443 stress/mockarca.jks changeit 0
# 2) Gateway apuntando al mock + SQL Server local (escucha en :18080)
java -jar target/tifactura-spring.jar \
--spring.config.additional-location=file:stress/application-stress.yml
# 3) Driver de carga — smoke primero
java -cp stress/out StressDriver n=2000 c=32 mode=v2 ente=1 suc=1 pos=1 run=1
n total de tx · c concurrencia · mode v1|v2 · ente/suc/pos identifican el POS · run hace únicos los tickets entre corridas.
Cómo se aísla cada cosa
- Contención del lock: mismo
ente+suc+pos→ todas las tx compiten por el mismosp_getapplock(numeracion:<ente>:<pv>:<tipo>). - Throughput sin contención: variar
pos→ PVs distintos = locks independientes, el throughput suma. - Dedup / carrera: dos drivers con el mismo
runen paralelo → deben devolver el mismo CAE, sin filas de más.
El pool Hikari dimensionado a la flota¶
Este es el ajuste de configuración que motivó la prueba. El perfil application-stress.yml sube el pool:
spring:
datasource:
hikari:
maximum-pool-size: ${DB_POOL_MAX:220} # 200 POS + headroom
connection-timeout: 30000
leak-detection-threshold: 60000
Hallazgo: dimensionar el pool al pico de POS simultáneos
Con 200 POS emitiendo en paralelo, un pool chico entra en connection-starvation: cada emisión en vuelo retiene una conexión (el persist-before-emit inserta la Trx en su propia transacción antes de llamar a AFIP — ver El ciclo de facturación). Si hay menos conexiones que POS activos, las emisiones se encolan esperando el pool, no la DB. Por eso el perfil de stress usa 220 = 200 POS + headroom.
Para producción, la regla que dejó la prueba es: dimensionar DB_POOL_MAX al pico de POS simultáneos, no a un número redondo. El default de operación normal (20, ver Configuración y perfiles) alcanza para un parque chico; una flota grande necesita subirlo. El pool también tiene que ser coherente con el max worker threads de SQL Server.
Además, el perfil apaga los jobs de fondo (spring.quartz.auto-startup: false) para que la reconciliación, PvSinMovimientos y CAEA no metan contención ajena a la medición, y baja el logging a WARN (loguear por request mata el throughput).
Resultados del smoke de validación¶
Corrida del 2026-07-03 contra DB local con el mock en latencia 0, validando end-to-end (mock + gateway + driver + SQL Server real + NumeracionLockSqlServer):
| Qué se midió | Resultado |
|---|---|
| Correctitud | 969 emisiones V2 al mismo PV con c=32 → 969 filas = 969 tickets distintos = 969 comprobantes distintos = 969 aprobadas. Cero doble-CAE, cero número repetido. |
| Serialización por PV | Mismo (ente, pv, tipo) → todos compiten por un único sp_getapplock; ~32 tx/s con latencia-mock 0. |
| Saturación del lock | Con c≥16 al mismo PV, ~3 % pega el timeout del applock (30 s, @LockTimeout). No es doble-emisión: es la cola saturada rechazando el sobrante — el comportamiento correcto (mejor rechazar que numerar mal). |
| PVs en paralelo | Dos PV distintos → locks independientes, el throughput suma. |
El número que importa para dimensionar producción
Con latencia ~800 ms (AFIP real) y el mismo PV, la emisión cae a ~1 tx/s por punto de venta: la llamada a AFIP ocurre dentro del lock de numeración, así que el CAE de un PV es intrínsecamente secuencial. El throughput real de producción se gana repartiendo la carga entre muchos PV, no acelerando un PV. La contención por-PV es a propósito: es lo que garantiza la numeración correlativa sin doble-CAE.
Qué mirar en cada corrida¶
- Throughput y latencias (p50/p95/p99) que reporta el driver.
- Lock: con latencia AFIP > 0, subí
cy observá cómo crece la p99 (la cola delsp_getapplock). - Hikari: si el pool se agota → esperas y timeouts de conexión; subí
DB_POOL_MAXo bajác. - Dedup:
SELECT COUNT(*), COUNT(DISTINCT nroTicketPos)sobre lasTrxdel run deben dar igual.
Por dónde seguir¶
- Persistencia — el
NumeracionLockSqlServer(sp_getapplock) y los índices calientes deTrx. - Configuración y perfiles — el pool Hikari y las env vars (
DB_POOL_MAX). - El ciclo de facturación — el
persist-before-emitque toma una conexión por emisión. - Invariantes sagradas — por qué la numeración se serializa y el dedup no puede fallar.