Aggiornamento di Discourse a Zeitwerk

Rails 6 include due modalità di autoloading: zeitwerk e classic. In quella pull request DEV: Upgrading Discourse to Rails 6 by KrisKotlarek · Pull Request #8083 · discourse/discourse · GitHub ho aggiornato Rails alla versione 6.0.0 con l’autoloader classic come fase transitoria. Sarebbe interessante provare a passare a Zeitwerk.

Zeitwerk è un caricatore di codice efficiente e thread-safe per Ruby. Finché il progetto segue le convenzioni di denominazione, Zeitwerk può individuare i file corretti e caricarli su richiesta o in anticipo, senza la necessità di alcun require o require_dependency. Inoltre, secondo quell’articolo https://weblog.rubyonrails.org/2019/2/22/zeitwerk-integration-in-rails-6-beta-2/, potrebbe offrire un piccolo incremento delle prestazioni all’applicazione.

Ci sono alcune operazioni che devo eseguire per farlo funzionare:

  1. Cambiare il nome di alcune classi per seguire la convenzione di denominazione di Rails. Ad esempio, il file canonical_url.rb dovrebbe definire la classe CanonicalUrl invece di CanonicalURL. Allo stesso modo, il file ondiff.rb dovrebbe definire la classe Onpdiff invece di ONPDiff. Un approccio alternativo sarebbe collegare un inflector personalizzato al progetto; tuttavia, penso che seguire la convenzione possa essere una scelta migliore - GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby · GitHub

  2. Analogamente al punto precedente, per convenzione le validazioni personalizzate presenti nella directory validations dovrebbero essere racchiuse nel modulo Validations. Inoltre, alcune validazioni ereditano da EachValidator e dovrebbero essere accessibili senza namespace. Intendo spostarle in una directory separata e aggiungerle ai percorsi di autoload.

  3. Rimuovere tutti i require_dependency e assicurarsi che il progetto funzioni correttamente.

  4. Rimuovere tutti i require e assicurarsi che Discourse funzioni correttamente.

  5. Assicurarsi che tutti i plugin possano accedere alle dipendenze richieste. Non so ancora come realizzare ciò. Vorrei prima far funzionare Discourse senza alcun plugin.

Naturalmente, ci sono ancora molte incognite da risolvere. Terrò aggiornati sui progressi; tuttavia, se siete interessati a dare un’occhiata, ho iniziato a sperimentare qui: Commits · KrisKotlarek/discourse · GitHub

Fatemmi sapere se notate eventuali svantaggi nell’implementazione di Zeitwerk o se pensate che abbia tralasciato qualcosa.

Ho fatto alcuni progressi con Zeitwerk, ma ho cambiato approccio. Il mio piano iniziale era modificare ogni luogo in cui Discourse non segue la convenzione di denominazione dei file di Zeitwerk. Dopo alcune correzioni, ho capito che questo era solo la punta dell’iceberg e ho notato che, se avessi seguito quella strada, la pull request sarebbe stata difficile da leggere e da fondere con sicurezza nel ramo master. Ad esempio, tutte le classi di job sotto una directory regolare dovrebbero avere lo spazio dei nomi Regular, lo stesso per Onceoff e Scheduled.

Ho deciso di fare un passo indietro e pensare a un approccio più evolutivo che rivoluzionario.

Ho deciso che sarebbe stato meglio introdurre un Inflector personalizzato, che copra tutti i file che non seguono la convenzione di Zeitwerk. Il vantaggio principale è che potremmo distribuire questa piccola modifica e, una volta soddisfatti di Zeitwerk e senza alcuna riduzione delle prestazioni, possiamo iniziare a correggere la convenzione file per file in pull request ragionevolmente piccole.

Ho trovato alcuni problemi che non potevano essere risolti con un Inflector personalizzato, quindi ho apportato ulteriori correzioni per farlo funzionare.

La pull request è ancora in corso, ma a questo stadio posso eseguire Discourse con Zeitwerk e i plugin predefiniti, eseguire tutti gli spec e i benchmark senza problemi.

Volevo prima raggiungere uno stato stabile in cui tutti gli spec passano. Ora posso iniziare con sicurezza a rimuovere tutti i require_dependency uno per uno e testare anche i plugin ufficiali. Una volta che tutto sarà pronto, condividerò con voi i risultati dei benchmark in questo post.

Per ora, se siete interessati ai progressi, potete dare un’occhiata a questa PR di bozza: DEV: Upgrading Discourse to Zeitwerk by KrisKotlarek · Pull Request #8098 · discourse/discourse · GitHub

Il file più importante è quell’Inflector personalizzato di Zeitwerk: DEV: Upgrading Discourse to Zeitwerk by KrisKotlarek · Pull Request #8098 · discourse/discourse · GitHub

Per far funzionare i plugin ho dovuto creare alcuni piccoli pull request. Una volta che saranno uniti, penso che i test su Discourse dovrebbero passare.

Ho anche verificato le prestazioni su Rails 6.0.0 con il caricatore automatico Classic e su Rails 6.0.0 con Zeitwerk.

Test Classic Zeitwerk Percentuale
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

I risultati non sono sempre coerenti, quindi li considererei con un po’ di cautela.

La quantità di incoerenza sulla mediana qui è un po’ strana, mi chiedo perché i risultati oscillino così tanto

Mi sorprende che il loader abbia un impatto

L’ho provato ancora una volta, ecco i risultati

Test Classic Zeitwerk Percento
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

Possiamo provare un altro metodo per il benchmark. Cosa ne pensi di più iterazioni, qualcosa che duri un’ora? Inoltre, invece di prendere il miglior risultato, confrontiamo la media di ogni esperimento. Questo potrebbe fornire numeri più coerenti. Che ne dici?

Nei miei pull request per i plugin, noterai che molte correzioni riguardano la ricerca nello spazio dei nomi globale.

Ho modificato codice come:

module ::Jobs
  class TranslatorMigrateToAzurePortal < Jobs::Onceoff

in:

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

Una cosa mi ha dato da pensare riguardo a quella soluzione: perché funzionava prima di Zeitwerk? Quella domanda, quando qualcosa funziona ma non dovrebbe, è sempre delicata :slight_smile:

Penso di aver trovato una possibile risposta nella descrizione del caricatore automatico classico (https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html#resolution-algorithms) - “Se non viene trovato, l’algoritmo risale la catena degli antenati del cref”.

Zeitwerk è più rigoroso. Una volta ho provato a caricare il codice prima della correzione e si lamentava che Jobs::Jobs::Onceoff non poteva essere trovato.

Sam ha suggerito nel pull request FIX: Use top-level namespace for base classes · discourse/discourse-prometheus-alert-receiver@ef9c238 · GitHub che, invece di usare < ::Jobs::Onceoff, possiamo semplicemente usare < Onceoff, e ha ragione. Ho verificato che senza lo spazio dei nomi funziona ugualmente. Penso che usare :: significhi esplicitare che ereditiamo da una classe del core di Discourse, tuttavia possiamo scegliere entrambe le strade.

Credo che quando il codice è così ravvicinato, questa lettura sia ottima:

module ::Jobs
  class TranslatorMigrateToAzurePortal < Onceoff

Se il codice inizia a distanziarsi, ha più senso essere espliciti… ad esempio:

module ::Jobs
  [ 50 righe omesse]
  class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff

Detto questo, sono indeciso, quindi sono a posto in entrambi i casi. ::Jobs::Onceoff è abbastanza breve e super esplicito, quindi possiamo semplicemente procedere così per ora.

Mi piacerebbe molto iniziare a far fondere questa modifica. @kris.kotlarek, ora è in uno stato pronto per la fusione? Il tempismo è ottimo perché abbiamo appena avuto una beta.

Lascia che io rebasi l’ultimo master su quello e mi assicuri che funzioni ancora; lo farò oggi.

@sam Penso che siamo pronti. Ho fatto il rebase con l’ultimo master e ho apportato una piccola modifica a Webauthn.
Ho verificato tre cose:
ho avviato il server in locale e ho cliccato un po’ per assicurarmi che funzionasse come previsto
ho eseguito gli spec
ho scaricato tutti i plugin ufficiali e ho verificato che gli spec per i plugin passino (dobbiamo prima unire le modifiche per i plugin)

Fresco, sto fondendo questa. Vediamo cosa ne viene fuori!

Possiamo fondere anche le correzioni per i plugin? Altrimenti, se provi a eseguire Discourse con i plugin, fallirà

Procedi pure con l’unione!

È stato unito! Se alcuni autori di plugin stanno avendo difficoltà, fatelo sapere in un nuovo argomento dedicato!

Ottimo lavoro, Kris!!!