Tots els articles

Disponibilitat en temps real: el problema difícil darrere de qualsevol sistema de reserves

Publicat 13 de set. del 20236 min de lectura
  • Enginyeria
  • Lueira
Disponibilitat en temps real: el problema difícil darrere de qualsevol sistema de reserves

Idees clau

  • La disponibilitat és un agregat calculat, no un valor desat: depèn d'accions de l'equip, reserves de clients, l'estat del material, edicions d'horari i holds que caduquen, sovint dins el mateix segon.
  • Consultar l'estat en viu en llegir sol ser més segur que mantenir una taula de disponibilitat precalculada, perquè una memòria cau només és tan correcta com el seu camí d'invalidació més feble.
  • Si fas servir memòria cau, lliga la invalidació al recurs i la finestra temporal exactes que han canviat: la inconsistència entre botiga, backoffice i vistes de reporting destrueix la confiança de l'equip molt ràpid.

El juny vaig escriure sobre com Lueira gestiona el moment en què algú confirma de veritat una reserva: instructor, material i franja horària bloquejats junts en una transacció atòmica, o res de res. Aquell article anava sobre el commit. Aquest va sobre tot allò que ha de ser cert abans que aquell commit tingui sentit: el número que veu un client, o una persona de l'equip, que diu que un recurs està "disponible" o "ocupat" en un moment donat. Aquest número resulta ser una de les coses més difícils d'encertar de manera consistent en tota la plataforma. En certa manera és més difícil que la mateixa transacció de reserva, perquè, a diferència d'una transacció, mai no es queda quiet.

La disponibilitat no és un fet, és un objectiu mòbil

L'instint, la primera vegada que modeles això, és tractar la disponibilitat com una propietat que pots llegir directament d'un registre: aquest monitor d'esquí està lliure a les 10, aquest parell d'esquís és en estoc, aquesta plaça en una classe col·lectiva encara té lloc. A la pràctica, "disponible" és un agregat calculat a partir de diverses fonts de veritat independents i concurrents, i qualsevol d'elles pot canviar la resposta:

  • Algú de l'equip bloqueja manualment una franja al calendari del backoffice, perquè un instructor ha trucat dient que està malalt o ha de marxar abans.
  • Un client completa una reserva en línia a la botiga, consumint l'última plaça o l'últim parell de botes d'una talla.
  • Un article es marca com a trencat o s'envia a reparar a mitja jornada, traient-lo del parc llogable.
  • Algú edita la plantilla d'horari setmanal recurrent —mou una classe, canvia l'aforament, afegeix una franja nova— i això es propaga cap endavant a totes les setmanes futures que hereten d'aquesta plantilla.
  • Un hold d'un checkout abandonat caduca, alliberant una plaça o un article que semblava compromès durant els últims deu minuts però que mai no va arribar a convertir-se en una reserva de veritat.

Un dimarts tranquil, aquestes coses gairebé mai no xoquen entre elles. Un dissabte al matí amb molt d'enrenou al taulell de lloguer, amb la botiga en línia també acceptant reserves i un instructor reorganitzant el seu dia, diverses d'elles poden passar dins el mateix segon. El sistema ha de donar una única resposta consistent, sigui quin sigui d'aquests cinc camins que hagi fet la pregunta en darrer lloc.

Consulta en viu, no confiïs que una memòria cau vagi al dia

La drecera temptadora és una taula de disponibilitat precalculada: una fila per recurs i franja horària, actualitzada cada vegada que alguna cosa canvia, llegida de manera barata des de la botiga. És temptador perquè és ràpid, i perquè sembla la forma natural d'aquestes dades. El problema és que cal invalidar-la correctament des de cinc camins d'escriptura independents, i n'hi ha prou que un d'ells s'oblidi d'actualitzar, o topi amb un altre, perquè el número en pantalla sigui incorrecte. I equivocar-se en aquest terreny no és cosmètic: significa fer overbooking d'un instructor, o dir a un client que un lloguer està disponible quan just s'ha marcat com a trencat.

El meu raonament per defecte aquí ha estat tractar la disponibilitat com una cosa que es calcula en llegir-la, contra l'estat actual, en lloc d'una cosa que es desa i s'espera que es mantingui sincronitzada. Una consulta en viu —encreuar el calendari del recurs, les seves reserves actuals, l'estat del seu material, els holds actius— és més cara per petició que una lectura de memòria cau, però és correcta per construcció: no hi ha lògica d'invalidació que es pugui espatllar, perquè no hi ha res quiet a invalidar. Per a una consulta que gairebé sempre toca els recursos i reserves d'un sol dia, aquest cost sol ser manejable molt abans de convertir-se en el coll d'ampolla.

Si fas servir memòria cau, fes-la estreta

Això no vol dir que fer servir memòria cau estigui descartat arreu: una botiga pública amb trànsit real acabarà necessitant alguna capa de memòria cau davant de la consulta en viu. Però el mode de fallada que més em preocuparia és una memòria cau el disparador d'invalidació de la qual sigui massa ampli: alguna cosa canvia, així que s'esborra i es reconstrueix tot. Això és senzill d'escriure i reintrodueix en silenci tots i cadascun dels problemes de correcció que té una taula precalculada, només que amb passos de més.

L'alternativa és indexar la memòria cau prou estretament perquè un disparador d'invalidació només hagi de raonar sobre el recurs i la finestra temporal exactes que han canviat, il·lustrat aquí com a pseudocodi simplificat en lloc de codi de producció real:

// ingenu: qualsevol escriptura a qualsevol lloc ho esborra tot
onAnyBookingWrite(event):
    cache.clear()  // correcte, però també llença tota la resta

// estret: invalida només el recurs/dia afectat
onInstructorAvailabilityChanged(event):
    key = availabilityKey(event.instructorId, event.date)
    cache.delete(key)

onBookingConfirmed(event):
    key = availabilityKey(event.resourceId, event.date)
    cache.delete(key)

onEquipmentStatusChanged(event):
    key = availabilityKey(event.itemId, event.date)
    cache.delete(key)

onHoldExpired(event):
    key = availabilityKey(event.resourceId, event.date)
    cache.delete(key)

La diferència no és només de rendiment. Un disparador estret és una cosa sobre la qual pots raonar i provar camí per camí: "marcar un article com a trencat invalida correctament la clau adequada?" és una pregunta amb una resposta clara de sí o no. "S'esborra sempre tota la memòria cau a temps?" és una pregunta sobre temporització i condicions de cursa entre cinc camins de codi no relacionats entre si, cosa molt més difícil de respondre amb confiança.

Una sola veritat, tres finestres

L'altra cosa que fa que aquest problema sigui especialment delicat a la pràctica és que la disponibilitat no es pregunta una sola vegada: es pregunta des d'almenys tres llocs diferents que han de coincidir: la botiga en línia on un client reserva una activitat o un lloguer, el calendari del backoffice on l'equip gestiona el dia, i les vistes de reporting internes que s'utilitzen per planificar capacitat. Si aquests tres alguna vegada discrepen, encara que sigui breument, algú se n'adona. Un cas concret que apareix constantment: un instructor decideix a mitja jornada, per un motiu personal, marcar-se com a no disponible la resta del dia. Això ha d'arribar a la botiga en línia en qüestió de segons —no a la següent sincronització programada— o la botiga vendrà alegrement una classe privada amb un instructor que ja se n'ha anat. El backoffice ho sap a l'instant, perquè és allà on es va fer el canvi; el difícil és assegurar-se que la resposta de la botiga a "aquest instructor és lliure a les 14h?" es derivi exactament del mateix estat, i no d'una fotografia de fa vint minuts.

Aquest és, al final, el veritable argument per calcular la disponibilitat en viu en lloc de mantenir vistes precalculades separades per superfície: si la botiga, el backoffice i el reporting resolen la disponibilitat mitjançant la mateixa consulta contra el mateix estat actual, estructuralment no hi ha on puguin discrepar. Qualsevol capa de memòria cau que s'afegeixi més endavant ha de preservar aquesta propietat, no esquivar-la.

Res d'això és enginyeria exòtica: no hi ha cap truc enginyós que faci desaparèixer la concurrència i els múltiples escriptors. El que de veritat ajuda és ser honest sobre quantes coses independents poden canviar "disponible" per sota teu, i dissenyar el sistema perquè pregunti de nou en lloc de fiar-se d'una resposta que va calcular fa un moment. Encertar la transacció de reserva, sobre la qual vaig escriure al juny, importa molt. Encertar el número que porta algú a intentar aquesta transacció en primer lloc importa igual de molt, i és la peça més fàcil d'espatllar subtilment sense adonar-se'n durant setmanes.