Tema representativo de entrevista

Entrevista técnica de código: ¿Cómo diseñar un iterador por lotes reanudable?

CodingIntermedio
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Diseña un iterador que recorra una API remota paginada elemento por elemento. Cada página tiene como máximo 100 elementos y utiliza un pageToken; las solicitudes pueden fallar o devolver duplicados, y el llamador guarda un cursor en puntos arbitrarios para reanudar más tarde. Explica la interfaz, los invariantes, el almacenamiento en búfer, la deduplicación, la semántica de recuperación, la complejidad y las pruebas.

Consigna y contexto

Diseña un iterador que recorra una API remota paginada elemento por elemento. Cada página tiene como máximo 100 elementos y utiliza un pageToken; las solicitudes pueden fallar o devolver duplicados, y el llamador guarda un cursor en puntos arbitrarios para reanudar más tarde. Explica la interfaz, los invariantes, el almacenamiento en búfer, la deduplicación, la semántica de recuperación, la complejidad y las pruebas.

El diseño de iteradores suele aparecer en el material público de entrevistas; la API de Java define hasNext() para verificar si hay otro elemento y next() para devolverlo o lanzar una excepción cuando no queda ninguno. Esta pregunta extiende el patrón familiar en memoria a un iterador remoto por lotes y reanudable, enfocándose en los límites de estado.

Qué evalúan los entrevistadores

Una respuesta promedio escribe un índice de arreglo. Una respuesta sólida separa el token de página, el índice dentro de la página, el elemento entregado y el punto de control confirmado por el llamador, y luego explica por qué los reintentos no pueden omitir ni duplicar datos de forma silenciosa. Las preguntas de seguimiento cubren páginas duplicadas, datos que cambian durante la paginación, una caída después de hasNext() y llamadas concurrentes.

La señal principal es el uso de invariantes para controlar un efecto secundario externo en lugar de tratar la paginación remota como un arreglo local.

Preguntas aclaratorias

  • ¿El orden es estable? Asume un orden (createdAt, id) inmutable; sin él, no se puede prometer una recuperación exacta.
  • ¿La recuperación es de tipo al menos una vez (at-least-once) o exactamente una vez (exactly-once)? Elige lecturas al menos una vez y permite que el llamador deduplique mediante un ID estable; el servicio remoto no tiene una transacción entre solicitudes.
  • ¿Se pueden insertar o eliminar filas? Asume que una instantánea (snapshot) o token de consistencia fija el conjunto de resultados; de lo contrario, promete solo un recorrido débil.
  • ¿Se permiten llamadas concurrentes? Por defecto, asume un uso de un solo hilo; las llamadas concurrentes necesitan un bloqueo o un error de estado explícito.
  • ¿Puede continuar la iteración después de una falla? Reintenta errores de red transitorios dentro de un límite; propaga de inmediato los errores de autenticación, de parámetros y de instantáneas expiradas.

Respuesta de 30 segundos

“Divido el cursor en una versión de instantánea, un token de página siguiente y un índice dentro de la página. El iterador almacena en caché una página; hasNext() no avanza el estado de entrega, mientras que next() consume un elemento y avanza el índice. El cursor persistido representa el último elemento entregado y confirmado por el llamador, por lo que la recuperación puede repetir un límite y es al menos una vez; el código receptor deduplica por ID estable. La API necesita un ordenamiento estable y una instantánea; de lo contrario, especifico una consistencia más débil. Las fallas de red tienen reintentos acotados y los errores permanentes se propagan.”

Respuesta paso a paso

Paso 1: Definir el estado y el contrato de la interfaz

EstadoSignificado¿Persistido?
snapshotConjunto de resultados fijo o versión de lectura
pageTokenCursor del servidor para la siguiente páginaSí, posiblemente vacío
indexSiguiente posición no entregada en la página actual
lastIdID estable del último elemento entregadoRecomendado

La interfaz puede exponer hasNext(), next(), checkpoint() y close(). hasNext() puede inspeccionar el búfer o precargar una página, pero no debe marcar un elemento como entregado. next() devuelve un elemento y avanza index. checkpoint() crea un token serializable; el llamador decide cuándo se confirma ese progreso.

Paso 2: Establecer los invariantes

text
0 <= index <= len(buffer)
next() returns buffer[index], then increments index
replace buffer and pageToken only after a whole page succeeds
the recovery token represents only the caller-acknowledged prefix
permanent errors are never swallowed by a retry loop

Si la solicitud de la página siguiente falla, conserva el búfer antiguo y la posición de entrega. Si una nueva página tiene éxito pero el proceso se cae antes de guardar un punto de control, la recuperación repite un sufijo, lo cual es al menos una vez. Guardar el punto de control antes de la entrega podría omitir un elemento, por lo que el orden importa.

Paso 3: Implementar lecturas de página y reintentos acotados

python
class ResumableIterator:
    def __init__(self, client, checkpoint=None, page_size=100):
        self.client = client
        self.page_size = page_size
        self.snapshot = checkpoint.snapshot if checkpoint else None
        self.token = checkpoint.page_token if checkpoint else None
        self.index = checkpoint.index if checkpoint else 0
        self.buffer = []
        self.done = False

    def has_next(self):
        self._ensure_buffer()
        return self.index < len(self.buffer)

    def next(self):
        self._ensure_buffer()
        if self.index == len(self.buffer):
            raise StopIteration
        item = self.buffer[self.index]
        self.index += 1
        return item

    def checkpoint(self):
        return Checkpoint(self.snapshot, self.token, self.index)

_ensure_buffer() solicita la siguiente página cuando se agota la página actual, utilizando un retroceso exponencial (exponential backoff) y un número máximo de intentos. Puede ocurrir un tiempo de espera agotado (timeout) después de una solicitud exitosa del lado del servidor, por lo que el reintento debe usar la misma instantánea/token y el servidor debe devolver una página estable o un límite de duplicado observable.

Paso 4: Manejar duplicados, inserciones y eliminaciones

Un token de página por sí solo podría no evitar datos duplicados después de un reintento. Si la API devuelve un id estable, descarta el prefijo en un límite de recuperación donde id <= lastId; para un ordenamiento compuesto, compara el cursor (createdAt, id) completo. No aumentes un conjunto de deduplicación sin límites; una instantánea del servidor y un token de límite mantienen la deduplicación local a la ventana de recuperación.

Sin una instantánea, puede aparecer una nueva fila antes de la página actual y una eliminación puede hacer que la página siguiente omita un elemento. Promete solo un recorrido de mejor esfuerzo del resultado visible, no consistencia de tipo exactamente una vez o fuerte. En una entrevista, reduce la garantía explícitamente o exige una versión de instantánea.

Paso 5: Definir la semántica de puntos de control y recuperación

Un punto de control debe contener la versión, la instantánea, el token, el índice dentro de la página, el último ID estable, un resumen de filtros (filter digest) y un tiempo de expiración. El resumen de filtros evita restaurar un cursor de una consulta en otra; la expiración evita leer silenciosamente un resultado diferente después de que el servidor libere una instantánea.

Reconstruye el iterador a partir del punto de control. Si el llamador guarda inmediatamente después de consumir item, la recuperación puede repetir ese elemento, por lo que las escrituras posteriores deben ser idempotentes por ID estable. Si el negocio requiere que no haya duplicados, el progreso y el resultado de negocio deben compartir una transacción o el almacenamiento posterior debe proporcionar una tabla de deduplicación; el iterador no puede crear exactamente una vez por sí mismo.

Paso 6: Complejidad, contrapresión y cierre

El espacio de búfer es O(page_size) y el avance local es O(1) por elemento. Las lecturas remotas son aproximadamente ceil(N / page_size), excluyendo reintentos. hasNext() puede emitir una solicitud de red, por lo que los llamadores no deben tratarlo como una operación gratuita. La precarga puede ocultar la latencia, pero debe limitarse a una página o a un presupuesto de bytes.

close() cancela el trabajo pendiente y libera la conexión; las instantáneas del servidor necesitan un TTL. Si un consumidor es más lento que el productor, la API debe limitar la tasa (rate-limit) o devolver un error de instantánea expirada en lugar de extender una instantánea indefinidamente. Las llamadas concurrentes deben rechazarse o serializarse, o dos llamadas a next() pueden observar el mismo índice.

Respuesta de muestra de alta calidad

“Modelaría el iterador remoto como una pequeña máquina de estados con snapshot, pageToken, buffer, index y lastId. hasNext() solo asegura que exista un elemento en el búfer; next() avanza index; checkpoint() almacena el prefijo confirmado por el llamador. Reemplaza el búfer solo después de que una página completa tenga éxito, reintenta tiempos de espera transitorios con un límite y propaga errores permanentes.

“Para la recuperación, requiero un ordenamiento estable y un token de instantánea. El punto de control también contiene el resumen de la consulta, el índice dentro de la página, el último ID estable y la expiración. La recuperación puede repetir el límite, por lo que prometo al menos una vez y hago que las escrituras posteriores sean idempotentes por ID. Sin una instantánea, las inserciones y eliminaciones debilitan la garantía.

“El búfer es O(pagesize), cada next es O(1) y las páginas remotas son aproximadamente ceil(N/pagesize). Las pruebas cubren páginas vacías, páginas duplicadas, tokens expirados, un tiempo de espera agotado después de un éxito del servidor, caídas durante puntos de control, mutaciones, recuperaciones repetidas, llamadas concurrentes a next, contrapresión y cierre. La semántica de exactamente una vez requiere una transacción compartida o un almacén de deduplicación.”

Errores comunes

  • Síntoma → Tratar la API remota como un arreglo y restaurar un solo índice entero → Por qué falla → Los límites de página y las mutaciones hacen que ese índice apunte a elementos diferentes → Solución → Persistir la instantánea, el token, el índice dentro de la página y el ID estable.
  • Síntoma → Avanzar el token en hasNext()Por qué falla → Un llamador puede inspeccionar sin consumir y luego caerse, omitiendo datos → Solución → Avanzar el estado de entrega solo después de que next() devuelva un elemento.
  • Síntoma → Pasar a la página siguiente después de un tiempo de espera agotado → Por qué falla → Se puede perder una página entera o se puede duplicar una solicitud exitosa → Solución → Reintentar el mismo token y deduplicar por ID estable.
  • Síntoma → Afirmar que el iterador garantiza exactamente una vez → Por qué falla → La persistencia del punto de control y los efectos secundarios del negocio no son una sola transacción atómica → Solución → Prometer al menos una vez y hacer que el destino sea idempotente o transaccional.
  • Síntoma → Precarga y reintentos ilimitados → Por qué falla → Los consumidores lentos agotan la memoria y las interrupciones bloquean indefinidamente → Solución → Acotar búferes, intentos, tiempos de espera y el TTL de la instantánea.

Preguntas de seguimiento y respuestas

¿Qué pasa si el servidor solo proporciona un número de página y no un token de instantánea?

Exige un cursor de clave compuesta estable o aclara que solo es posible un recorrido débilmente consistente. Los números de página cambian después de inserciones y eliminaciones, por lo que no pueden garantizar que no haya omisiones ni duplicados.

¿Qué pasa si el destino acepta cada elemento solo una vez y no puede deduplicar?

El iterador no puede garantizar exactamente una vez por sí solo. Coloca el progreso, la escritura de negocio y el punto de control en una sola transacción, o exige un destino idempotente; de lo contrario, incluye posibles duplicados en el contrato.

¿Qué pasa si una página sigue agotando el tiempo de espera repetidamente?

Conserva el búfer antiguo y no avances el token. Tras alcanzar el límite de reintentos, genera un error clasificado para que el llamador elija pausar, omitir o reiniciar. Una omisión debe registrar una brecha y no puede pasar silenciosamente a la página siguiente.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta