< Volver 

Rod and Reef: Temporadas

2026-09-14

Temporadas de 45 días contadas desde una época fija, cuatro leaderboards por temporada sobre OrderedDataStores y títulos permanentes para el top 50. Ningún servidor coordina con otro: la temporada es una función del reloj y el cierre es una tarea idempotente.

LuauRobloxBackendDataStore
Ir a vista rápida

El tiempo partido en temporadas de 45 días.

Advertencia

Rod and Reef todavía no ha sido lanzado, y no se pueden mostrar videos ni GIFs de este sistema hasta que salga.

Resumen. Rod and Reef tiene temporadas competitivas con cuatro tablas de clasificación: peces atrapados, monedas ganadas, experiencia y mejor racha de capturas. Al cerrar cada temporada, los 50 mejores de cada tabla reciben un título permanente con su puesto y el número de la temporada. El problema de fondo es que un juego de Roblox corre en muchos servidores a la vez, sin estado compartido y sin un proceso central, y todos tienen que estar de acuerdo en qué temporada es, cerrar cada una una sola vez en efecto, y hacerlo sin gastarse el presupuesto de peticiones que también usan las cargas de datos de los jugadores. Este artículo describe cómo lo resolví. La temporada actual es una función pura del reloj: una división entera desde una época fija. Los contadores de cada jugador se reinician de forma perezosa, la primera vez que se tocan en la temporada nueva. El cierre no usa candados: cada servidor puede calcularlo, el cálculo es idempotente y dos servidores que compiten escriben lo mismo. Y todas las lecturas pasan por cachés con deduplicación de peticiones en vuelo, para que mil jugadores abriendo la misma tabla cuesten una sola petición cada 30 segundos.

1. El problema

La primera versión usaba meses calendario, con identificadores como 2026-08. Funcionaba, pero los meses miden entre 28 y 31 días, y lo que queríamos era una cadencia propia y temporadas del mismo largo. Además el sistema tenía que cumplir varias cosas a la vez. Todos los servidores tienen que coincidir en la temporada actual sin hablar entre sí. Las tablas tienen que guardarse por temporada, y las de temporadas pasadas no pueden cambiar. Un jugador que estuvo desconectado durante un cierre tiene que recibir su título igual. Y nada de esto puede competir con lo único que de verdad no puede fallar, que es cargar los datos del jugador al entrar.

2. El reloj

2.1 La temporada como función del tiempo

Sea E la época, el 11 de agosto de 2026 a las 00:00 UTC, y Δ=45⋅86400=3888000 segundos el largo de una temporada. El índice de la temporada en el instante t es

k(t)=max⁡(0, ⌊t−EΔ⌋)

El máximo con cero hace que un reloj atrasado nunca produzca una temporada anterior a la primera. Veinte temporadas forman un ciclo, así que

c=⌊k20⌋+1,s=(kmod20)+1

y la temporada se identifica como C<c>S<s>. El jugador ve el nombre largo, “Cycle 1 Season 2”, y todo lo que se guarda o se usa para armar una clave usa el identificador corto, que no tiene espacios. Un ciclo dura 900 días, casi dos años y medio. La función inversa es k=20(c−1)+(s−1), con la validación 1≤s≤20, y la temporada k termina en E+(k+1)Δ, que es también cuando empieza la siguiente. Hoy, 2 de octubre de 2026, k=1: es la temporada C1S2, que corre del 25 de septiembre al 9 de noviembre.

2.2 Sin coordinación

Como k depende solo del reloj, todos los servidores calculan la misma temporada sin mandarse un mensaje, salvo durante los segundos en que sus relojes no coinciden alrededor de un borde [1]. No hay un proceso que anuncie el cambio de temporada, porque no hace falta: el cambio es un instante, y cada servidor lo nota la próxima vez que pregunta.

Los identificadores viejos del formato mensual no se interpretan como ninguna temporada, y un identificador que no nombra ninguna temporada se trata igual que el vacío de un perfil nuevo: se resuelve a la temporada actual. Así, migrar los perfiles al formato nuevo no necesitó ningún script.

2.3 El número que llevan los títulos

Los títulos muestran el número de la temporada, como “(S7)”. Ese número es k+1, contado desde la época a través de todos los ciclos, y no el número de temporada dentro del ciclo. Si fuera el del ciclo, la primera temporada de dos ciclos distintos marcaría el mismo “(S1)”, y dos títulos ganados en temporadas diferentes se verían idénticos.

3. Los contadores

3.1 Reinicio perezoso

Cada perfil guarda un bloque de temporada: el identificador de la temporada a la que pertenecen sus contadores, los peces atrapados, las monedas ganadas, la experiencia, la racha actual, la mejor racha y la lista de temporadas ya procesadas para títulos. Cuando empieza una temporada nueva no hay ningún trabajo que recorra los perfiles para ponerlos en cero, que con perfiles guardados en un DataStore sería imposible de todas formas. Cada evento que toca los contadores revisa primero si el identificador del perfil coincide con la temporada actual, y si no coincide lo actualiza y pone todo en cero:

idperfil≠id(k(t))⇒contadores←0,  idperfil←id(k(t))

Es un reinicio perezoso de costo constante por evento. La revisión va antes de tocar cualquier contador, y eso cerró un bug: un evento podía llegar entre la carga del perfil y la rutina de entrada del jugador, y en ese momento el perfil todavía tenía el identificador vacío de la plantilla. El incremento se le atribuía a una temporada que la rutina de entrada iba a reiniciar un momento después, y lo ganado en esa ventana se perdía.

3.2 Eventos y no sondeo

El servicio no revisa nada periódicamente. Escucha las señales del juego: una captura, un pez perdido, monedas ganadas y experiencia ganada. Atrapar suma uno a los peces y a la racha actual, y actualiza la mejor racha con un máximo. Perder un pez pone la racha actual en cero. Cada cambio se sincroniza con el cliente y marca al jugador como pendiente de escribir en las tablas.

4. Las tablas

4.1 Una tabla ordenada por temporada

Cada tabla de cada temporada es un OrderedDataStore propio, con el nombre del tipo de tabla y el identificador de la temporada (RAR_Season_Fish_C1S2, por ejemplo), donde la clave es el UserId y el valor es el contador. Separar una tienda por temporada hace que las temporadas cerradas no cambien por construcción, porque nadie vuelve a escribir en el nombre de una temporada que ya terminó, salvo una escritura atrasada que la sección 7 discute.

Escribir en cada evento gastaría el presupuesto en minutos. Los jugadores con cambios quedan marcados, y cada 30 segundos se escriben sus cuatro valores y su nombre visible. Al salir un jugador se intenta una última escritura, pero con una espera acotada, una carrera entre la escritura y un temporizador de 5 s, para que una petición colgada no deje enganchado el manejador de salida. Al apagarse el servidor se espera a todas las escrituras pendientes hasta 25 s, dentro de los 30 que Roblox le da a un servidor que se cierra.

4.2 Leer sin agotar el presupuesto

Los DataStores tienen un presupuesto de peticiones por minuto y por servidor, y cuando la cola se llena el motor descarta peticiones, incluidas las cargas de datos de jugadores que están entrando. Todo el trabajo de fondo de las temporadas, antes de cada petición, espera a que haya presupuesto para ese tipo de petición, revisando cada medio segundo, con un tope de unos 150 s para no quedarse esperando para siempre.

Las lecturas, además, se comparten. Una tabla resuelta (los 50 mejores con sus nombres) se guarda 30 s y se le sirve a todos los que la pidan en ese tiempo. La página cruda de la tabla, sin nombres, se comparte también con la tarjeta de perfil, que muestra el puesto del jugador y se abre mucho más seguido que la tabla. Y si dos jugadores piden la misma tabla mientras la primera petición todavía está en vuelo, el segundo recibe la misma promesa y no dispara otra. Con eso, el número de lecturas de una tabla por servidor queda acotado por

lecturas≤⌈tiempo30 s⌉

sin importar cuántos jugadores la abran. Los nombres visibles se guardan en caché por servidor, porque cambian muy poco.

Un OrderedDataStore solo entrega páginas ordenadas desde arriba, así que el puesto de un jugador se conoce solo si está entre los 50 primeros. Saber que alguien es el 5213 obligaría a recorrer todas las páginas de la tienda. Y como las escrituras salen cada 30 s, el puesto va hasta medio minuto detrás de los contadores en vivo, pero es el mismo dato que muestra la tabla, así que las dos cosas siempre coinciden, que importa más que ser instantáneo.

5. El cierre

5.1 Sin candados

Cuando una temporada termina hay que calcular sus 50 mejores por tabla y guardarlos. No hay un servidor encargado de eso, y elegir uno requeriría coordinación. En vez de eso, todos los servidores lo intentan. Cada uno, 60 s después de arrancar y luego cada 300 s, mira las tres temporadas anteriores a la actual, y para cada una que no haya procesado todavía:

  1. Lee los resultados guardados de esa temporada, a través de la caché compartida.
  2. Si ya existen, no hace nada.
  3. Si no, pide las páginas ordenadas de las cuatro tablas, toma los 50 primeros de cada una y escribe los cuatro resultados en una sola actualización de la clave de esa temporada.

Antes eran cuatro escrituras sobre la misma clave, una por tabla. Juntarlas en una cuesta una cuarta parte y evita que la clave quede a medio escribir.

5.2 Por qué no hace falta un candado

El cálculo es una función de la temporada y del contenido de sus tablas, f(k), y una temporada cerrada ya no recibe escrituras. Si dos servidores compiten, los dos leen las mismas tablas y calculan la misma lista, y la escritura mezcla la lista en lo que ya hay, así que escribir dos veces el mismo valor deja el mismo estado que escribirlo una:

w(w(S,v),v)=w(S,v)

La operación es idempotente, y el resultado es el mismo con uno, dos o diez servidores compitiendo. Es la misma idea que permite que los sistemas con consistencia eventual reintenten sin miedo [2, 3]: si repetir no cambia nada, no hace falta asegurarse de hacerlo una sola vez. Cada servidor guarda además qué temporadas ya procesó, para no volver a intentarlo en cada vuelta, y si un intento falla quita la marca para que la vuelta siguiente lo reintente.

El cierre espera 60 s después del arranque por una razón práctica. Calcularlo es una ráfaga de lecturas de tablas ordenadas y una escritura por temporada, y hacerlo mientras todos los jugadores están entrando competía con las cargas de sus perfiles y las dejaba sin presupuesto.

6. Títulos

Al entrar, cada jugador revisa las tres temporadas anteriores que todavía no tiene marcadas como procesadas. Para cada una lee los resultados (de la caché, así que es gratis después del primer jugador del servidor), y si todavía no existen no la marca, para revisarla de nuevo la próxima vez que entre. Si existen, busca su UserId en la lista de cada tabla. La lista está ordenada de mayor a menor, así que su posición es su puesto.

El título se guarda con una clave compuesta, tabla y temporada (por ejemplo Fish@C1S2), junto con el puesto. Una clave simple como the_sea_king ya estaría puesta la segunda vez que alguien la gana, y el título tiene que ser un registro permanente y distinto por temporada. Se muestra como “#3 SEA KING (S2)”, el jugador ve la pantalla de recompensa y la temporada queda marcada como procesada, de modo que procesarla otra vez no vuelve a otorgar nada. Hay un título por tabla: SEA KING para los peces, GOLD SOVEREIGN para las monedas, GRAND MARINER para la experiencia y RELENTLESS KING para la racha.

7. Limitaciones

Hay una ventana de hasta 30 s al final de cada temporada. Un jugador que atrapa un pez justo antes del borde todavía tiene su perfil en la temporada vieja, y su escritura sale en la siguiente vuelta de 30 s, hacia la tabla de la temporada vieja. Si otro servidor cerró esa temporada en esos segundos, ese último cambio no entra en el resultado. Si la lectura de una de las cuatro tablas falla durante el cierre, esa tabla se escribe como una lista vacía para no perder las otras tres, pero como después el resultado ya existe nadie lo vuelve a calcular, así que esa tabla queda vacía para esa temporada. Un jugador que no entra durante más de tres temporadas, 135 días, ya no recibe los títulos de las que quedaron atrás. Y el puesto fuera del top 50 no se puede conocer con un OrderedDataStore.

Las dos primeras tienen arreglos conocidos: esperar unos minutos después del borde antes de cerrar, y no guardar el resultado de una tabla que falló para que se reintente. No los he hecho todavía.

8. Lo que me dejó

Tratar el tiempo como una función pura eliminó toda la coordinación entre servidores, y el reinicio perezoso eliminó el único trabajo que habría tenido que recorrer todos los perfiles. Lo más útil, sin embargo, fue la idea de las tareas idempotentes: en vez de garantizar que algo pase exactamente una vez, que en un sistema distribuido es difícil, diseñar la tarea para que hacerla dos veces sea lo mismo que hacerla una, y entonces dejar que la haga cualquiera. Y la última lección fue sobre recursos compartidos. Las temporadas son una función secundaria, y casi todo su código existe para no quitarle presupuesto a lo que sí es crítico.

Referencias

  1. L. Lamport. “Time, Clocks, and the Ordering of Events in a Distributed System”. Communications of the ACM, 21(7), 1978.
  2. W. Vogels. “Eventually Consistent”. Communications of the ACM, 52(1), 2009.
  3. M. Kleppmann. Designing Data-Intensive Applications. O’Reilly, 2017.

Este texto de Mastedore. Todos los derechos reservados.