Skip to content

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 ADMONUso
NombreBaseDatosNombre de la base de datos del tenant
ServidorSQLHostname o IP del servidor SQL Server
ContrasenaServidorSQLContraseñ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:

FormatoEjemplo
Azure SQL (tcp:host,port)tcp:mi-server.database.windows.net,1433
IP desnuda172.16.0.22
host:portsql-server.local:1433
host,portsql-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 TTL
func (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_size
SELECT COUNT(*) FROM (consulta_base) AS _count_query
SELECT columnas
FROM (consulta_base) AS _paged_query
ORDER BY [columna_sort] ASC|DESC
OFFSET (page - 1) * page_size ROWS
FETCH NEXT page_size ROWS ONLY

La 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 LIKE
whereClause := 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 SQL

Si 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 loop
scanBuffer := 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.CatUsuarios sin 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.ExecuteQuery soporta 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, StreamQuery es la operación correcta. Opera con memoria constante mediante buffers reutilizables y chunking configurable.