Plastic internals: de 3.0 a 4.0

1:38 0 Comments

Hoy va a ir de programación y estructuras de datos :). Voy a explicar en qué consiste el cambio fundamental en el que hemos estado trabajando en Plastic 4.0. Un cambio “core” de verdad que tiene mucho impacto y que creo que es muy interesante. Afecta al rendimiento, al sistema de réplica (lo que nos hace un DVCS), al mantenimiento del código, a la usabilidad…

Os habéis fijado en el nuevo Branch Explorer de 4.0? Dibujo una comparativa debajo:

Además de que los gráficos son mejores, ¿qué más veis? Los cuadrados de 3.0 son changesets y se han convertido en óvalos en 4.0, más grandes, porque ahora son más importantes. Pero hay otro tema: hay unas flechas entre los changesets que antes no existían. Esas flechas indican “parentesco”.

Es decir, ahora Plastic “sabe” que un changeset es padre de otro, mientras que antes no se guardaba esa relación.

Pero voy a bajar más, a las profundidades del sistema.

Almacenamiento de versiones

Un control de versiones guarda los cambios que haces en un árbol de código. Es decir, tú tienes un proyecto con ficheros y directorios, y por cada cambio se van guardando “revisiones” (que tienen muchas utilidades, desde volver atrás, revisar, etc, etc, pero no es el tema ahora).

Un ejemplo como el siguiente:

  • Primero tenemos un repositorio vacío.
  • Luego añadimos /src y /src/foo.c
  • Luego se modifica foo.c

    ¿Cómo se guarda eso en Plastic?

    ¿Qué hay en la base de datos de Plastic?

    Instalad 3.0, con el backend de base de datos que más os guste (ya sabéis, Firebird, SQLite, MySQL, SQLServer… hay unos cuantos) y ejecutad las siguientes queries:

    “select * from revision”

    “select * from item”

    “select * from childrenitem”

    En lugar de conectaros al backend, es posible ejecutar las consultas con el comando “cm query” o “cm q” (hay que tener el permiso “advanced query” para poder hacerlo, cuestiones de seguridad).

    Si veis las tablas que usa plastic veréis que son muy pocas. Ahora me centro sólo en tres que se pueden definir con el siguiente diagrama entidad-relación (no muy ortodoxo).

  • Cada elemento en el sistema es un ítem. Es decir, el directorio raíz (/) del ejemplo anterior es un ítem, “src” es un ítem y “foo.c” es un ítem. Pero un ítem es una “entrada abstracta”, es “que existes” pero no tienes ni un nombre asociado. (Ahora os he perdido, pero para dejarlo sencillo, un ítem es “la clase” y una revisión es “el objeto”).
  • Un ítem puede tener una o varias revisiones. Es decir, de “foo.c” podemos tener miles de revisiones (por eso sacar la historia en plastic es muy sencillo: “select * from revision where fiditem=xxx”).
  • Un ítem puede representar a un fichero o a un directorio.
  • Una revisión, si es fichero, puede ser texto o binario, si es directorio sólo directorio (es un poco más complejo porque también soportamos links y cosas así, pero de momento sirve).
  • Si la revisión es un fichero, tendrá un dato asociado (tabla revisiondata, los datos divididos en blobs de máximo 4Mb).
  • Si la revisión es un directorio entonces empieza lo bueno: puede tener una o más entradas “childrenitem”.

    Al principio dije que “el ítem es abstracto” porque no tiene ni nombre ni datos. La revisión tampoco conoce su nombre, el nombre, en realidad, se guarda en el childrenitem.

    Me explico: un fichero “foo.c” puede llamarse más adelante “bar.c” pero sigue siendo el mismo ítem, es más, ni siquiera se crea una revisión nueva del fichero al hacer un renombrado, sino una nueva versión del directorio que lo contiene y que le da nombre.

    ¿Ahora se entiende mejor?

    Volviendo al ejemplo original:

  • Al principio sólo tenemos un ítem (el “famoso” ítem raíz) y una revisión (revisión cero) de ese ítem raíz en el changeset 0.
  • Entonces añadimos src y src/foo.c. ¿Qué quiere decir? Que se añaden dos ítems nuevos y dos revisiones nuevas.

    ¿Qué ocurre con los “children ítems”? Aquí viene lo interesante.

    En el changeset 0 la tabla childrenitem está vacía.

    En el changeset 1 la tabla childrenitem tiene el siguiente aspecto (recordad, acabamos de añadir /src y /src.foo.c al sistema):

    ¿Qué quiere decir?

  • Hemos creado la “revisión 1” del directorio raíz (antes era la revisión 0, se crea una nueva al añadir “src”).
  • Esa revisión 1 (DirRevId) tiene como dato una entrada llamada “src”, que es el itemId 10.
  • A su vez el directorio “src” tiene una primera revisión (revisión DirRevId = 2 en este caso (estoy usando un nº global para referirme a las revisiones, que es lo que usa plastic internamente)) que tiene como dato una entrada llamada “foo.c” del itemId 11.

    Es decir, el directorio raíz es el ítem 0. “src” es el ítem 10. “foo.c” es el ítem 11. Sabemos que hemos creado una nueva revisión de “/” (raíz) que es la “1”. Y una revisión de “src” que es la “2” (no incluyo la revisión creada con los datos de foo.c porque no tiene que ver con “childrenitem”).

    De este modo es como Plastic asocia los nombres de elementos (ítems) a entradas de directorio concretas.

    Un segundo checkin

    En el changeset 2 lo que ocurre es que se crea una nueva revisión del fichero src/foo.c.

    ¿Cómo se altera la tabla “revision”? Simple: se crea una nueva entrada para almacenar el nuevo cambio de foo.c

    ¿Cómo se altera la tabla “childrenitem”? No se modifica. No hay cambio en la estructura, luego no se modifica.

    El selector entra en escena

    Si habéis usado Plastic habréis visto alguna vez que cada workspace tiene asociado un “selector”. Ese selector es un texto que le indica al sistema “qué debe cargar” en el workspace. Es algo como:
    repository "doc@diana.codicefactory.com:9092"
      path "/"
        branch "/main"
        checkout "/main"
    

    Para muchos esto lo que hace es que le dice a Plastic que debe trabajar en la rama “main” y listo.

    Pero en realidad no es tan simple.

    Al estudiar “childrenitem” habréis visto que hay una relación entre el directorio y los ítems que debe cargar pero… ¿cómo sabemos qué revisión de un ítem determinado se debe cargar?

    Es decir, si yo cargo la revisión 1 del root ítem, sé que debo cargar una entrada del ítem “src” (ítem 10). Esa información me la da la primera línea de “childrenitem”. Pero, ¿qué revisión de “src” debo cargar?

    Es más, si nuestro repositorio ahora mismo (changeset 2) es algo como (en plan diagrama de objetos):

    ¿Cómo sabemos qué se debe “montar” como árbol de ficheros en un momento dado?

    Es ahí donde entra en acción el selector. Las siguientes reglas:

    repository "doc@diana.codicefactory.com:9092"
      path "/"
        branch "/main"
        checkout "/main"
    

    En realidad quieren decir: “carga lo último de la rama “main” para todo lo que esté bajo el path “/” “.

    Al resolver un selector Plastic lo primero que hace es buscar el “root ítem”:

  • ¿Está el root ítem? Sí, es el ítem 0. (En caso contrario se daría un error fatal)
  • Cuál es la última revisión del root ítem? En nuestro caso la revisión 1.
  • Ok, coger la revisión 1.
  • ¿Tiene datos? – Sí, es un directorio y como “dato” tiene la entrada de children ítem “src”.
  • ¿Qué ítem es src? – Es el ítem 10.
  • Ahora el proceso se repite recursivamente para cada directorio: ¿cuál es la última revisión de “src”? Es la revisión 2.
  • ¿Tiene datos la revisión 2? Sí, tiene una entrada “foo.c –item 11”.
  • ¿Cuál es la última revisión de foo.c? Es la revisión 4, la que hemos creado en el changeset 2.

    Por tanto, el selector anterior cargaría el siguiente árbol:

    Que es el árbol que se resuelve en el changeset último en este momento o changeset 2.

    (No he incluido el manejo de ramas en todo el ejemplo, pero sería lo mismo, realmente el selector no dice “cuál es la última revisión del ítem 10” sino “cuál es la última revisión del ítem 10 en la rama main”)

    Qué pasaría si ahora modificásemos el selector de la siguiente forma:

    repository "doc@diana.codicefactory.com:9092"
      path "/"
        branch "/main" changeset “1”
        checkout "/main"
    

    Ocurriría que la respuesta a las preguntas anteriores nos daría el siguiente árbol:

    Destacando lo que ha cambiado respecto al selector anterior.

    Un grafo dinámico

    Es decir, Plastic NO puede resolver un árbol de directorios si no se le indica un “selector” que le ayude a “seleccionar” la revisión adecuada al ir cargando cada nivel de directorios.

    La carga de árboles (resolución de selector o “solvepath” como le llamamos en el equipo) no funciona exactamente como la he descrito. Es decir, sí que funciona así a nivel lógico pero está optimizada para minimizar las consultas a la base de datos, utiliza cachés intermedias, etc, etc (ha sido uno de los campos de optimización desde los primeros pasos del proyecto).

    Este grafo dinámico da una enorme flexibilidad: un movido, un renombrado, se trazan a nivel de directorio, pero no se tiene que crear una nueva revisión de los ficheros implicados (o directorios).

    Mediante esta estructura (y un uso adecuado de selectores) es como se implementa el sistema de “herencia de ramas”:

  • Al cargar la “última revisión de /main/tarea001” de foo.c lo que se hace es: ¿hay una revisión en la rama “tarea001”? Sí, cogerla.
  • No? Entonces coger la última (o la correspondiente según la estructura de la “smartbranch” task001) de la rama “padre” /main.

    Y así es como funciona la herencia de ramas…

    Los selectores y la estructura de gráfico dinámico dan una gran flexibilidad: es posible tener estructuras diferentes del árbol montando “diferentes reglas”: podrías coger un directorio de una rama, que cargue el contenido de los ficheros de otra, montando una estructura totalmente dinámica.

    En realidad una cosa es lo que “existe” en el repositorio y otra lo que el usuario “ve” en el workspace, muy al estilo de las “vistas” del viejo ClearCase.

    Como veis, el sistema hasta 3.0 tenía una enorme flexibilidad.

    Cambios en 4.0

    Pero a cambio de esa “flexibilidad” también había una gran “complejidad”. Por un lado se necesita un tiempo para acostumbrarse al mecanismo “grafo dinámico” aunque mediante nuestras herramientas intentamos hacerlo lo más transparente posible.

    Por otro, impone ciertas complicaciones a la hora de trabajar en modo distribuido (DVCS), como por ejemplo garantizar que un changeset en un repositorio es exactamente igual al replicarlo a otro, porque dependerá de las revisiones que existan en el destino y de cómo se carguen. Un pequeño lío.

    Además la carga, aunque optimizada, era pesada…

    ¿¿Qué hemos cambiado en 4.0??

    Pues en realidad usamos la misma base de datos!!!

    En serio, la misma estructura, pero con un pequeño “hack” en cuanto al significado.

    ¿Recordáis la tabla childrenitem?

    Pues hay un pequeño cambio en 4.0: en lugar de que cada entrada apunte a un itemid, apunta a un revid… (¡!!!!)

    ¿Qué significa esto?

    Significa que nos ahorramos la fase de resolución porque ahora cada árbol es “hardcoded” una vez que se hace checkin. Un changeset tiene una estructura fija, más sencilla de entender, más rápida y eficaz, mucho mejor en la réplica. No hay que interpretar una serie de reglas para resolver un árbol.

    Perdemos algo de flexibilidad, es cierto, no se pueden hacer maravillas como modificar un directorio en una rama, cambiando el nombre a un fichero y nada más, y poder montar un selector complejo que altere el nombre de un ítem concreto sólo en nuestra vista jugando con esas ramas. No puede hacerse y me dolió mucho quitarlo pero… era una flexibilidad con un gran poder y… muy poca responsabilidad ;)

    4.0 es más “DAG” (algún Git fan por aquí?), más rápido, más simple, y este pequeño cambio (un solo significado dentro de una tabla) define un nuevo mecanismo de merge, de réplica, de checkin, de diferencias… de sincronización con otros SCMs… de todo!!!

  • 0 comentarios: