Gateway Multitenant SQL Server
El gateway multitenant es el núcleo funcional de GM Hades Backend para el ecosistema SQL Server de GM Transport. Permite que cualquier backend autenticado envíe una consulta SQL junto con un RFC de cliente y reciba los resultados, sin conocer las credenciales ni la ubicación de la base de datos destino.
Identificación del tenant
Cada tenant se identifica por su RFC (Registro Federal de Contribuyentes), el identificador fiscal mexicano de 10 a 13 caracteres. El RFC se pasa en cada petición como parte del TenantContext en v2, o como campo de primer nivel en v1.
El RFC es el único dato que el cliente necesita proporcionar para acceder a la base de datos del tenant. Las credenciales, el servidor y el nombre de la base de datos son responsabilidad exclusiva del gateway.
Resolución de credenciales
Las credenciales de cada tenant están almacenadas en la base de datos ADMON en la tabla CatUsuarios. El CredentialsRepository ejecuta una consulta parametrizada con sql.Named para obtener tres campos:
| Campo en ADMON | Uso |
|---|---|
NombreBaseDatos | Nombre de la base de datos del tenant |
ServidorSQL | Hostname o IP del servidor SQL Server |
ContrasenaServidorSQL | Contraseña de conexión |
La columna TipoUsuario restringe el acceso a los tipos 2, 3 y 13. Un RFC con un tipo diferente devuelve codes.NotFound.
Formatos de servidor soportados
El campo ServidorSQL en ADMON puede estar en cuatro formatos, todos manejados por parseServerString:
| Formato | Ejemplo |
|---|---|
Azure SQL (tcp:host,port) | tcp:mi-server.database.windows.net,1433 |
| IP desnuda | 172.16.0.22 |
host:port | sql-server.local:1433 |
host,port | sql-server.local,1433 |
Caché de credenciales en proceso
Para evitar una consulta de red al ADMON en cada petición, el CredentialsRepository mantiene un mapa en proceso con TTL de 15 minutos (configurable con MULTITENANT_CACHE_TTL_MINUTES):
type CachedCredentials struct { Config *ClientDatabaseConfig ExpiresAt time.Time}
// Lookup con verificación de TTLfunc (r *CredentialsRepository) GetClientDatabaseConfig(ctx context.Context, rfc string) (*ClientDatabaseConfig, error) { if cached, ok := r.cache[rfc]; ok && time.Now().Before(cached.ExpiresAt) { return cached.Config, nil } // Cache miss: consulta a ADMON config, err := r.fetchFromADMON(ctx, rfc) if err != nil { return nil, err } r.cache[rfc] = &CachedCredentials{Config: config, ExpiresAt: time.Now().Add(r.cacheTTL)} return config, nil}El máximo de tenants en caché es configurable con MULTITENANT_MAX_IN_MEMORY (default 100). Esta es la principal reducción de latencia para cargas de trabajo repetitivas del mismo tenant.
Ejecución de consultas
DatabaseRepository.ExecuteQuery soporta las siguientes capacidades sobre el driver go-mssqldb:
Paginación
-- Generado automáticamente a partir de pagination.page y pagination.page_sizeSELECT COUNT(*) FROM (consulta_base) AS _count_query
SELECT columnasFROM (consulta_base) AS _paged_queryORDER BY [columna_sort] ASC|DESCOFFSET (page - 1) * page_size ROWSFETCH NEXT page_size ROWS ONLYLa consulta de conteo se ejecuta primero para poblar total_rows en la respuesta, permitiendo que el cliente calcule el número total de páginas sin una segunda llamada.
Búsqueda dinámica
Cuando se especifica search.term, se construye una cláusula WHERE dinámica sobre las columnas indicadas:
// Los términos de búsqueda se escapan reemplazando ' por ''// antes de interpolarse en el patrón LIKEwhereClause := fmt.Sprintf( "WHERE %s LIKE '%%%s%%'", strings.Join(searchColumns, fmt.Sprintf(" LIKE '%%%s%%' OR ", escapedTerm)), escapedTerm,)Metadata de columnas
Cuando return_metadata=true, se invoca rows.ColumnTypes() después de ejecutar la consulta. Cada columna retorna:
- Nombre
- Tipo nativo de la base de datos (
DatabaseTypeName) - Precisión y escala (para tipos numéricos)
- Longitud máxima (para tipos de cadena)
- Nullabilidad
Estadísticas de ejecución
Cuando return_statistics=true, la respuesta incluye execution_time_ms (tiempo total de la consulta en milisegundos) y row_count (filas retornadas en la página actual, no el total).
Propagación de timeout y cancelación
Cada operación de base de datos opera bajo un context.Context derivado del contexto gRPC de la petición. El timeout se aplica en dos niveles:
context.WithTimeout(ctx, tenantTimeout) ← nivel de petición └─ context.WithTimeout(ctx, queryTimeout) ← nivel de ejecución SQLSi el cliente gRPC se desconecta, el contexto de la petición se cancela automáticamente. El db.QueryContext(ctx, ...) del driver SQL Server detecta la cancelación y aborta la consulta en el servidor, liberando inmediatamente los recursos de base de datos.
Cancelación explícita por el cliente
El handler v2 mantiene un sync.RWMutex-protected map de queries activos:
type activeQuery struct { ID string RFC string SQL string StartedAt time.Time CancelFunc context.CancelFunc}El cliente puede cancelar una consulta en ejecución llamando a CancelQuery con el query_id devuelto en ExecuteQueryResponse. El handler recupera el CancelFunc del mapa y lo invoca, propagando la cancelación al driver.
Tamaño máximo de mensaje
El servidor gRPC se configura con un límite de 100 MB para envío y recepción (grpc.MaxRecvMsgSize, grpc.MaxSendMsgSize). Este límite existe para consultas que retornan conjuntos de datos grandes en una respuesta unaria. Para datasets que superan este límite, StreamQuery es la operación correcta.
Streaming de grandes conjuntos de datos
StreamQuery itera el cursor SQL fila por fila con un buffer pre-asignado de []interface{} que se reutiliza en cada iteración, evitando allocaciones por fila en el hot path:
// Pre-allocación fuera del loopscanBuffer := make([]interface{}, len(columns))for i := range scanBuffer { scanBuffer[i] = new(interface{})}
for rows.Next() { if err := rows.Scan(scanBuffer...); err != nil { return err } // Construir row y acumularlo if len(batch) >= batchSize { if err := sendChunk(stream, batch); err != nil { return err } batch = batch[:0] // Reutilizar el slice }}Una vez que batch acumula batchSize filas, se invoca stream.Send(). El cliente recibe chunks incrementalmente sin esperar al final del resultado.
Resumen
- El tenant se identifica por su RFC; las credenciales se resuelven desde
ADMON.CatUsuariossin que el cliente las conozca. - La caché en proceso con TTL de 15 minutos elimina la consulta de red al ADMON en peticiones repetidas del mismo tenant.
DatabaseRepository.ExecuteQuerysoporta paginación con conteo previo, búsqueda dinámica con LIKE escapado, sort dinámico, metadata de columnas y estadísticas de ejecución.- Los timeouts se propagan desde el contexto gRPC hasta el driver SQL Server. La desconexión del cliente aborta la consulta en el servidor de forma inmediata.
- Para datasets que superan 100 MB en una respuesta unaria,
StreamQueryes la operación correcta. Opera con memoria constante mediante buffers reutilizables y chunking configurable.