Actualización de Discourse a Zeitwerk

Rails 6 incluye dos modos de autocarga: zeitwerk y classic. En esa solicitud de extracción DEV: Upgrading Discourse to Rails 6 by KrisKotlarek · Pull Request #8083 · discourse/discourse · GitHub, actualicé Rails a la versión 6.0.0 con el autocargador clásico como fase de transición. Sería interesante intentar cambiar a Zeitwerk.

Zeitwerk es un cargador de código eficiente y seguro para hilos para Ruby. Mientras el proyecto siga las convenciones de nomenclatura, Zeitwerk puede encontrar los archivos correctos y cargarlos bajo demanda o de antemano, sin necesidad de usar require o require_dependency. Además, según ese artículo https://weblog.rubyonrails.org/2019/2/22/zeitwerk-integration-in-rails-6-beta-2/, podría proporcionar un pequeño aumento de rendimiento a la aplicación.

Hay algunos pasos que debo realizar para que funcione:

  1. Cambiar el nombre de algunas clases para seguir la convención de nomenclatura de Rails. Por ejemplo, el archivo canonical_url.rb debería definir la clase CanonicalUrl en lugar de CanonicalURL. De manera similar, el archivo ondiff.rb debería definir la clase Onpdiff en lugar de ONPDiff. Una alternativa sería conectar un inflector personalizado al proyecto; sin embargo, creo que seguir la convención podría ser una mejor opción: GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby · GitHub

  2. Al igual que en el punto anterior, por convención, las validaciones personalizadas que residen en el directorio validations deben estar envueltas en el módulo Validations. Además, algunas validaciones heredan de EachValidator y deben ser accesibles sin espacio de nombres. Planeo moverlas a un directorio separado y agregarlas a las rutas de autocarga.

  3. Eliminar todos los require_dependency y asegurarse de que el proyecto funcione correctamente.

  4. Eliminar todos los require y asegurarse de que Discourse funcione correctamente.

  5. Asegurarse de que todos los plugins puedan acceder a las dependencias requeridas. Aún no sé cómo lograrlo. Quiero primero hacer que Discourse funcione sin ningún plugin.

Por supuesto, aún quedan muchas incógnitas por resolver. Mantendré informados sobre el progreso; sin embargo, si están interesados en echar un vistazo, comencé a experimentar con esto aquí: Commits · KrisKotlarek/discourse · GitHub

Por favor, háganme saber si ven alguna desventaja al implementar Zeitwerk o si creen que me he olvidado de algo.

He avanzado un poco con Zeitwerk, aunque he cambiado mi enfoque. Mi plan original era modificar cada lugar donde Discourse no sigue la convención de nombres de archivos de Zeitwerk. Tras aplicar algunas correcciones, me di cuenta de que esto es solo la punta del iceberg y noté que, si seguía por ese camino, la solicitud de extracción (pull request) sería difícil de leer y de fusionar con confianza en master. Por ejemplo, todas las clases de trabajo ubicadas en directorios regulares deberían tener el espacio de nombres Regular, lo mismo ocurre con Onceoff y Scheduled.

Decidí dar un paso atrás y pensar en un enfoque más evolutivo que revolucionario.

Pensé que sería mejor introducir un Inflector personalizado que abarque todos los archivos que no siguen la convención de Zeitwerk. La mayor ventaja será que podremos desplegar ese pequeño cambio y, una vez que estemos satisfechos con Zeitwerk y no haya ninguna degradación del rendimiento, podremos comenzar a corregir la convención archivo por archivo en solicitudes de extracción razonablemente pequeñas.

Encontré algunos problemas que no podían resolverse con el Inflector personalizado, por lo que realicé correcciones adicionales para que funcionara.

La solicitud de extracción aún está en progreso, pero en esta etapa puedo ejecutar Discourse con Zeitwerk y los plugins predeterminados, ejecutar todas las pruebas y realizar las pruebas de rendimiento sin problemas.

Quería llegar primero a ese estado estable en el que todas las pruebas se superaran. Ahora puedo comenzar con confianza a eliminar todos los require_dependency uno por uno y también probar los plugins oficiales. Una vez que todo esté listo, compartiré con ustedes los resultados de las pruebas de rendimiento en esta publicación.

Por ahora, si les interesa el progreso, pueden echar un vistazo a esta solicitud de extracción en borrador: DEV: Upgrading Discourse to Zeitwerk by KrisKotlarek · Pull Request #8098 · discourse/discourse · GitHub

El archivo más importante es ese Inflector personalizado de Zeitwerk: DEV: Upgrading Discourse to Zeitwerk by KrisKotlarek · Pull Request #8098 · discourse/discourse · GitHub

Para que los plugins funcionen, tuve que crear algunos pull requests pequeños adicionales. Una vez que se fusionen, creo que las pruebas en Discourse deberían pasar.

También verifiqué el rendimiento en Rails 6.0.0 con el cargador automático Classic y en Rails 6.0.0 con Zeitwerk.

Prueba Classic Zeitwerk Porcentaje
categories-50 32 26 81.25
categories-75 37 29 78.38
categories-90 47 35 74.47
categories-99 67 49 73.13
home-50 30 29 96.67
home-75 37 31 83.78
home-90 44 40 90.91
home-99 67 52 77.61
topic-50 35 35 100.00
topic-75 36 36 100.00
topic-90 48 36 75.00
topic-99 57 58 101.75
categories_admin-50 51 48 94.12
categories_admin-75 62 50 80.65
categories_admin-90 89 66 74.16
categories_admin-99 135 101 74.81
home_admin-50 48 47 97.92
home_admin-75 58 49 84.48
home_admin-90 67 64 95.52
home_admin-99 101 81 80.20
topic_admin-50 48 48 100.00
topic_admin-75 55 49 89.09
topic_admin-90 63 65 103.17
topic_admin-99 92 69 75.00
load_rails 2617 2165 82.73
rss_kb 282428 315684 111.78
pss_kb 270491 303504 112.20

Los resultados no siempre son consistentes, así que los tomaría con un grano de sal.

La cantidad de inconsistencia en la mediana aquí es un poco extraña; me pregunto por qué los resultados fluctúan tanto.

Me sorprende que el cargador tenga algún impacto.

Lo intenté una vez más, aquí están los resultados

Prueba Clásico Zeitwerk Porcentaje
categories-50 25 25 100.00
categories-75 26 26 100.00
categories-90 37 33 89.19
categories-99 57 48 84.21
home-50 26 26 100.00
home-75 27 28 103.70
home-90 38 35 92.11
home-99 60 50 83.33
topic-50 27 26 96.30
topic-75 35 27 77.14
topic-90 41 33 80.49
topic-99 54 50 92.59
categories_admin-50 48 50 104.17
categories_admin-75 60 61 101.67
categories_admin-90 76 71 93.42
categories_admin-99 122 122 100.00
home_admin-50 47 46 97.87
home_admin-75 58 55 94.83
home_admin-90 66 63 95.45
home_admin-99 99 121 122.22
topic_admin-50 50 49 98.00
topic_admin-75 62 50 80.65
topic_admin-90 72 65 90.28
topic_admin-99 103 74 71.84
load_rails 2675 2216 82.84
rss_kb 279924 315240 112.62
pss_kb 267659 303026 113.21

Podemos probar otra forma de realizar las pruebas de rendimiento. ¿Qué te parecería hacer más iteraciones, algo que se ejecute durante una hora? Además, en lugar de tomar el mejor resultado, compara el promedio de cada experimento. Eso podría dar números más consistentes. ¿Qué opinas?

En mis solicitudes de extracción para complementos, notarás que muchas correcciones se centran en la búsqueda en el espacio de nombres global.

Cambié código como

module ::Jobs
  class TranslatorMigrateToAzurePortal < Jobs::Onceoff

por

module ::Jobs
  class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff

Una cosa me molestaba de esa solución: ¿por qué funcionaba antes de Zeitwerk? Esa pregunta, cuando algo funciona pero no debería, siempre es complicada :slight_smile:

Creo que encontré una posible respuesta en la descripción del autocargador clásico (https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html#resolution-algorithms): “Si no se encuentra, el algoritmo recorre la cadena de ascendientes del cref”.

Zeitwerk es más estricto. Una vez que intenté cargar el código antes de corregirlo, se quejaba de que Jobs::Jobs::Onceoff no se podía encontrar.

Sam sugirió en la solicitud de extracción FIX: Use top-level namespace for base classes · discourse/discourse-prometheus-alert-receiver@ef9c238 · GitHub que, en lugar de usar < ::Jobs::Onceoff, podríamos simplemente usar < Onceoff, y tiene razón. Verifiqué que funciona igual sin el espacio de nombres. Estoy pensando que usar :: indica explícitamente que heredamos de una clase del núcleo de Discourse, pero podemos optar por cualquiera de las dos opciones.

Creo que cuando el código está tan cerca, esto se lee muy bien:

module ::Jobs
  class TranslatorMigrateToAzurePortal < Onceoff

Si el código empieza a separarse, tiene más sentido ser explícito… por ejemplo:

module ::Jobs
  [ 50 líneas omitidas]
  class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff

Dicho esto, estoy indeciso, así que me parece bien de cualquier manera. ::Jobs::Onceoff es lo suficientemente corto y mega explícito, así que podemos quedarnos con eso por ahora.

Me encantaría empezar a fusionar esto. @kris.kotlarek, ¿está ahora en un estado fusionable? El momento es bastante bueno porque acabamos de tener una beta.

Déjame rebasar el master reciente en eso y asegurarme de que aún funciona. Lo haré hoy.

@sam Creo que estamos listos. He rebaseado con la rama master más reciente y realicé un pequeño ajuste en Webauthn. Verifiqué tres cosas:
Ejecutar el servidor en local y hacer algunas pruebas para asegurar que funciona como se espera
Ejecutar las pruebas (specs)
Descargar todos los plugins oficiales y asegurar que las pruebas de los plugins estén pasando (primero necesitamos fusionar los ajustes para los plugins)

¡Genial! Voy a fusionar esto. ¡Veamos qué sale!

¿Podemos también fusionar las correcciones para los plugins? De lo contrario, si intentas ejecutar Discourse con plugins, fallará

¡Adelante y fusiona!

¡Esto ya está fusionado! Si algún autor de plugins tiene dificultades, ¡avísanos en un nuevo tema dedicado!

¡Gran trabajo, Kris!!!