in tech

Monorepo

Dva týdny zpátky jsme ve VersionPressu přešli na monorepo, tj. jedno velké repo namísto řady menších. Byl to interně trochu kontroverzní krok a i na webu se najdou silné názory na obě strany, tak sem hodím pár našich poznámek a zkušeností. (Ahoj Alice!)

Proč monorepo?

Jednou větou hlavně proto, že nás už štvala režie kolem správy více repozitářů. V reálu jen jejich kombinace reprezentovala projekt jako celek, jedna logická změna byla často v několika pull requestech v různých repech, nebylo vždy jasné, kde vůbec založit issue, atd.

Zbytečný overhead.

Na co myslet

Monorepo některé věci komplikuje. Konkrétně:

  • CI je náročnější na správně nastavení. Např. zbuildit je potřeba jen změněné věci a detekce toho je složitější.
  • Labely u issues potřebují víc struktury a disciplíny.
  • Emailové notifikace jsou komplikovanější. GitHub umožňuje sledovat jen mentions, assignments apod., ale úplně srovnatelné se sledováním samostatného repa to není.
  • Sdílení repa se subkontraktory budeme muset nějak pořešit, nejspíš subtree splitem a manuální synchronizací; nemám s tím zkušenost a trochu tady očekávám opruz.

U opravdu velkých rep pak může být problém s jejich škálováním, ale tam ještě zdaleka nejsme a kdoví, jestli vůbec někdy budeme. Zajímavé by bylo slyšet od Jakuba Vrány, jak jim to funguje v Googlu (tam je nejen monorepo, ale i single line of history).

Po dvou týdnech jsme zatím spokojení.

Jak mergnout repa

Tady je skript, který jsme několikrát použili (jsem Bash lama, ale funguje to):

Push je normálně do branche, otevřeme k tomu (trošku větší 🙂 ) pull request, můžou se pak udělat ještě nějaké dodatečné úpravy např. v README a nakonec merge.

Máte někdo s monorepem zkušenosti a dobré rady pro nás ostatní?

Write a Comment

Comment

17 Comments

  1. Facebook začínal na SVN. Když jsem tam byl, tak bylo SVN pořád autoritativní, třeba se z něj dělaly releasy. Ale všichni vývojáři používali Git, který aktualizovali z centrálního Git mirroru SVN (nepoužívali tedy git-svn). Se SVN jsi přišel do styku, jen když se dělaly patche už hotového release, což navíc často někdo udělal za tebe.

    Pak Facebook postupně začal přecházet na Mercurial. Hlavní důvod byl, že pro udělání potřebných výkonových změn nebylo mezi vývojáři Gitu moc pochopení, takhle velké repozitáře moc nechtěli podporovat. Udělat změny do čistěji navrženého Mercurialu bylo jednodušší, vývojáři Hg za ně navíc byli rádi. A pak na rozhodnutí taky hrálo roli, že Facebook jednoho významného vývojáře Hg najal…

    Hlavní repozitáře byly ve Facebooku dva – www (frontend, hlavně PHP a JS) a fbcode (backend v C++ a dalších jazycích). Některé open-source projekty měly repozitář myslím vlastní.

    Single line of history je u větších repozitářů podle mě nezbytnost. Má to tak Facebook i Google. Mergnout velký feature branch je riskantní operace – všechno máš odladěné, ale při mergi máš konflikty, které musíš vyřešit, což ti to může zase celé rozbít. Hezky to popisuje https://secure.phabricator.com/book/phabflavor/article/recommendations_on_branching/.

    Google kdysi běžel na Perforce, pak si to forknul na Piper. Detaily jsou na https://www.wired.com/2015/09/google-2-billion-lines-codeand-one-place/.

    Sjednocení do velkého repozitáře je podle mě rozhodně krok správným směrem. Já v Google často dělám velké refaktoringy a představa, že bych je musel honit po víc repech, je šílená. Někdy bych si přál, aby celý GitHub bylo jedno velké repo a já mohl poslat jeden pull-request, třeba když změním nějaké API Closure. Na dělání to po jednotlivých projektech sílu nemám.

    • Díky za výborné doplnění.

      Námět na tvůj blog by mohlo být trochu se rozepsat o single line of history. Nikdy jsem s tím modelem na velkém projektu nepracoval a moc si ho v detailech nedokážu představit, např. když se mění rozhraní třídy, jestli to je nějak o-ifované, nebo souběžně existuje SomeClass a SomeClass2? Apod.

      • Rozhraní máme dvou druhů: Buď mají jednu, dvě implementace, tak ty se změní najednou s rozhraním. Nebo jsou široce používaná a ta jsou zakonzervovaná a prakticky se nemění.

        Teď třeba přesouvám jeden typ z Closure Templates do Closure. Dočasně existují oba a jeden je zděděný od druhého. Postupně posílám pull requesty (v terminologii Gitu) na změnu kódu z jednoho na druhý. Až to bude hotové, původní typ smažu.

        Nebo když měním API nějaké metody, tak ji nechám zpětně kompatibilní. Pokud to nejde, tak ji přejmenuju. Např. místo setHtml(string) zavedu setSafeHtml(SafeHtml), převedu kód a starou smažu.

        Když se přidává nová funkcionalita, která nemá být vidět všemi, tak je to oifované. Někdy se třeba všem posílají data tam i zpátky, jako by to bylo už spuštěné, jen se to nezobrazuje v UI. Tomu se říká dark-launch – vyzkouší se výkon a okrajové případy, ale dá se to snadno vypnout, aniž by si toho někdo všiml.

        V Google taky řešíme skrývání JS kódu funkčností, které ještě nejsou veřejné. V minulosti se nám stalo, že někdo deobfuskoval JS kód Gmailu a podle toho zjistil, co se chystá nového. Napsal o tom článek a když jsme to pak pustili, tak už to niko nezajímalo… Děláme to v zásadě tak, že všechna takováhle funkčnost je vyčleněna bokem do něčeho, čemu říkáme delegates. V hlavním kódu je pak jen delegate.renderPage(params) nebo něco podobného. Delegate buď nemusí udělat nic nebo může něco vyrenderovat. A když se bundluje JS kód, tak se uživatelům pošle jen s těmi delegáty, které mají zapnuté. Při psaní kódu je s tím docela opruz: pokud pro něco ještě neexistuje delegát, tak ho musíš vyrobit, navěsit ho do kódu a pak teprve implementovat. Ale na druhou stranu je funkčnost hezky vyčleněná bokem, takže když se pak nakonec třeba něco nespustí, tak je jednoduché to smazat.

          • Ano, všechno jde hned do masteru. Branche se používají jen na release. Když se v release přijde na nějakou chybu, tak se do ní patchne (down-integrate) oprava. Oprava je prakticky vždy jen rollback toho commitu, který něco rozbil, případně všeho, co na něm závisí. Release se dělá celkem často, ve Facebooku to bývalo dvakrát denně, v Gmailu je to trochu složitější – máme i hourly releasy, ty se ale nasazují jen testerům a vývojářům Gmailu, kteří o to stojí. Když je nějaký problém s hourly, tak se prostě zahodí. Pak jsou daily releasy, které jdou i některým Googlerům a které se už opravují. A z nich pak vznikají releasy, které se nasazují postupně všem.

            Všechny změny musí být zpětně i dopředně kompatibilní. JS kód musí počítat s tím, že mu na serveru poběží starší i novější verze. S protocol buffery je to naštěstí celkem jednoduché – když přidáš optional pole, tak s ním můžeš pracovat, i když ho server nepošle (a vrátí se ti null). Stejně tak nevadí, když server posílá něco, s čím klient ještě nepočítá.

        • Napsal o tom článek a když jsme to pak pustili, tak už to niko nezajímalo…

          To je nějaká urban legend, ne? 🙂 Článek, který přečetli všichni na světě.

          • Nezajímalo to novináře. Psali o tom stylem: „Google konečně spustil něco, o čem jsme vás už před měsícem informovali.“ Což má dva důsledky – jednak to novináře přestane rychle zajímat a přestanou o tom psát a jednak ty články pak ani nezajímají lidi. Nad tím příběhem prostě ztratíš kontrolu a většinou zbude jen pachuť ve stylu: „No kóóónečně, to vám to trvalo.“

  2. Předpokládám že máte kód rozdělený do balíčků/projektů/knihoven, přestože je všechen v jednom repu. Integrujete je pak přímo přes cestu na disku, nebo přes package manager (npm)? Máte jeden master build, který dokáže zbuildit všechny projekty v repu, ve správném pořadí?

      1. Zkoušeli jsme obojí, zatím je to cesta na disku (nemáme potřebu exportovat balíčky samostatně). Důležité je v praxi i to, jak si s tím poradí nástroje, aby třeba spolehlivě fungoval refaktoring napříč různými moduly. Tam zatím ideální cestu hledáme.
      2. To je plán, zatím není rozsah projektu moc velký a build / nasazení jednotlivých věcí ve správné pořadí hlídá člověk (jednotlivé moduly mají build automatizovaný). Jak by měl master build skript vypadat, jsme už dost do detailu probrali (jak říkáš, buildění ve správném pořadí, rebuild jen věcí, které se od posledně změnily, apod.), ale záměrně se tomu zatím vyhýbáme, protože jsme se na CI už několikrát spálili a důležitější jsou teď jiné věci.