Pereiti prie pagrindinio turinio

Angies kūrimo užkulisiai: sudėtinga sistema, kuri slepiasi po gražiu UI / II dalis

Tęsiame tinklaraščio įrašų seriją apie tai, kaip buvo kuriama Angis. Šį kartą įrašas techninis: kodėl naudojama front-end’inė Python kompiliatoriaus implementacija, kodėl reikėjo perrašyti visą tokenizerį, kaip buvo kuriamas kodo tikrinimo robotas ir ko kurdami platformą nesitikėjome. . Apie visa tai ir dar daugiau pasakoja Mantas Urbonas. Bet apie viską nuo pat pradžių…

Angies portalo techniniai iššūkiai blog'p įrašas

Angis.net platformą sudaro kelios dalys:

  • Dinaminis portalas, kuriame skelbiami nauji lygiai ir rodomi video
  • Kodo redaktorius, kur galima rašyti / trasuoti Python kodą
  • Online Python kompiliatorius-interpretatorius, naršyklėje vykdantis Python programą
  • Python-Javascript-multimedija biblioteka, leidžianti Pythono programoms naršyklėje judinti spraitus, apdoroti vaizdą ir groti mp3 bei animacijas
  • Failų saugykla serveryje, kur laikomi vartotojo parašytos programos su resursais
  • Užduočių verifikatorius (irgi serveryje), kuris tikrina vartotojo parašytą kodą ir nustato, ar lygis pereitas ar ne.

Visos šios dalys turi sklandžiai veikti tam, kad žmonės šią platformą galėtų rasti internete, peržiūrėti mokomąją video medžiagą, su Python programavimo kalba  kurti savo 2D programas, kuriamam kodui gauti teisingų automatinių patarimų. Angyje Python’u parašyta programa mokiniai gali dalintis su neregistruotais lankytojais, t.y., programos kodą ir resursus – gali matyti ir neautorizuoti vartotojai. Sukurtos programos veikia kompiuterių, planšečių, telefonų ir net žaidimų konsolių naršyklėse.

Kiekviena iš šių sistemos dalių kažkada atrodė „paprasta ir lengva”, ir – kaip atrodė tuomet –  tereikėjo gerai padaryti šešias dalis, ir viskas bus baigta per porą mėnesių. Kaip mes klydom! Kiekviena dalis labai greitai išsivystė į nelauktai sudėtingą reikalą. 

Atrodytų, jog „portalas” – tai yra Angular + gražus UI + vaizdo įrašų grotuvas + daug funkcionalumo – yra lengva ir banaliai įprasta, tiesa? Taip iš pradžių ir buvo, kol nepasipylė netikėti siurprizai iš „kaimyninių posistemių”, kaip antai Python kompiliatoriaus, failų saugyklos ar IDE kaprizai (jau nekalbant apie nusipirkto UI toolkit’o nesuderinamumą su Angular). Šiuo atveju gelbėjo gerai žinomas SaaS pasaulis, turiu omeny, kad žinojome, kaip teisingai dėlioti CI/CD, rašyti unit testus, mokėjom technologijas ir metodikas. Taigi iššūkiai šiai platformos vietai tebuvo nuolatos auganti ir besikeičianti apimtis.

Visiškai kitokia situacija buvo su Python’o kompiliatoriumi. Python kompiliatorių modifikavime turėjome gana nedidelę patirtį. Todėl nusprendėme vietoje standartinio back-endin’io Python’o naudoti frontend’inę implementaciją, t.y., kad „veiktų tiesiai naršyklėje”. Tokiu būdu lengva programą trasuoti tiesiai naršyklėje, ir viskas veikia net laikinai sutrikus serveriui ar interneto ryšiui. Iš panašių implementacijų pasirinkome Skulpt, nes atvirojo kodo ir populiari. Pasipylė siurprizai 🙂

Pirma, Skulpt nedraugavo su Unicode parašytu tekstu – mat tokenizeris parašytas remiantis Javascript’o regexp’u, todėl lietuviškų kintamųjų vardų ar funkcijų pavadinimų tokenizavimas nebesigauna. O Angis privalo leisti vaikams kodinti sklandžiai lietuviškai! Tad ir tokenizerį teko perrašyti. O Skulpte tokenizeris, lekseris, parseris ir interpreteris trupučiuką persipynę… ir, be abejo, nėra jokios dokumentacijos. 

Tokenaizeriai/lekseriai/parseriai yra ganėtinai sudėtingas dalykas, o programuojant tokius sudėtingus dalykus ypač svarbu algoritmo našumas. Pavyzdžiui, Žilvinas, pašalinęs vieną mažai reikalingą funkcionalumą, padidino greitaveiką daugiau nei 100 kartų. 

Taigi, kompiliatorių programavimas yra įdomu ir smagu, bet sunkiausia dalis dar tik laukė. Mums būtinai reikėjo automatinio kodo vertintojo – tokio „roboto”, kuris tikrintų mokinių užduotis. Angyje dešimtys tūkstančių vaikų mokosi programuoti, rašydami savo programėles, kurias kažkas turi patikrinti: ar naudotojas teisingai suprato užduotį, ir ar spręsdamas panaudojo teisingas priemones. 

Pavyzdžiui, „for-ciklo“  pamokoje naudotojas turi parašyti tam tikrą programą, privalomai panaudodamas for-ciklą – jokiu būdu ne while-ciklą, ir ne dublikuodamas kodo. Kai kuriose užduotyse kodo reikia rašyti gana daug, mokiniai turi labai daug laisvės, daug ką keisti ir perdarinėti, o „robotas-tikrintojas” turi patikrinti, ar mokinys suprato pamokos temą ir ar teisingai pritaikė pamokos žinias.

Paminėsiu dar porą reikalavimų užduočių tikrinimo robotams:

  • Tikrinama programa piešia spraitus, tikrina kolizijas, groja animacijas ir t.t.
  • Tikrintojas turi pereiti visas vykdymo šakas, t.y., if-elseif-else , tuščus ciklus, išbandyti visus pelės ir klaviatūros įvesties rezultatus.
  • Pelės ir klaviatūros reagavimas, be abejo, yra asinchroninis. 
  • Parašytos programos negali nulaužti tikrintojo. Tai yra, reikia identifikuoti amžinus ciklus, begalines rekursijas, masyvo indekso ribas, atminties rijikus ir t.t.
  • „Sugriuvęs” tikrintojas turi atsistatyti automatiškai, nenulauždamas serverio.
  • Tikrintojas turi kaip galima tiksliau paaiškinti naudotojui, kur padaryta klaida.
  • Tikrintojas turi suveikti kaip galima greičiau, t.y. tūkstančiams žmonių jų kodo tikrinimas turi trukti porą sekundžių.
  • i18n , be abejo.

Pirmiausia turėjome nuspręsti kuriuo būdu tikrinsime užduotis. Paprasčiausia idėja remtis unit testais ir tikrinti užduoties rezultatus netiko, nes reikėjo patikrint, kaip naudotojai išmoko naudoti specialias sintaksines/semantines struktūras (pvz. ciklą). Tada svarstėme galimybę nagrinėti arba tekstą tiesiogiai arba abstract syntax tree. Tačiau ir šis variantas netiko, nes paprastai neįmanoma nustatyti ar kodas bus vykdomas ar ne (pvz.: ciklas yra metode, kuris niekada nekviečiamas). Galiausiai nusprendėme keisti Pythono interpretatorių: generuojamame kode vykdymo metu siunčiant pranešimus apie vykdomą abstract syntax tree mazgą. Šiuo būdu mes galėjome vykdyti pateiktą užduotį ir gauti informaciją apie realiai naudojamas sintaksines struktūras.

Toks sprendimas reiškė, kad mūsų robotas turės vykdyti trečių šalių kodą su visomis iš to išplaukiančiomis pasekmėmis. Mes norėjome, kad vienas procesas be restarto galėtų patikrinti daugiau nei vieną sprendinį. Pirma reikėjo sėkmingai susitvarkytų su amžinais ciklais ir panašiomis DoS tipo atakomis (net ir atsitiktinėmis). Tada užtikrinti, kad vykdomi skriptai neįtakotų kitų skriptų būsenos arba globalios būsenos. Kaip dažniausiai būna – paprasčiausias sprendimas yra geriausias sprendimas. NodeJS turi specialų modulį “vm”, kurio pagalba galima sukurti “izoliuotą” globalų kontekstą ir jam vykdyti kodą (panašu į AppDomain senajame .NET Framework’e, tik nereikia MarshalByRefObject norint bendrauti tarp kontekstų). Ir dar geriausia, kad galėjome pilnai palaikyti “vm” modulį. Kiekvienam sprendimo tikrinimui buvo suteiktas 60 sekundžių limitas – po to būdavo metama klaida.

Norint teisingai vykdyti pateiktą sprendinį mums reikėjo naršyklės… Arba bent jau apsimesti, jog naršyklė egzistuoja. Beveik visą darbą už mus atliko JSDOM biblioteka. Tiesa norint, kad teisingai užkrautų animacijas, paveiksliukus/spraitus, skaičiuotų kolizijas ir t.t., dar teko naudoti canvas biblioteką. 

Turėjome parašytii savo resource loader’į, kuris visus resursus krovė lokaliai, taip išvengiant kreipinių per tinklą. Kad animacijos eitų greičiau, perėmėme su laiku susijusias funkcijas (setTimeout, setInterval ir requestAnimationFrame) bei modifikavome susijusius objektus (Date, performance) – laiką “pagreitinome” 16 kartų. 

Visa Angies infrastruktūra veikė Azure platformoje todėl natūralu, kad tikrinimo robotas išnaudojo serverless servisus. Mes iš anksto žinojome, kad tikrinimas gali ilgai užtrukti (iki 60s ir daugiau jei įskaičiuotume pasileidimo laiką) todėl tikrinimas turėjo būti vykdomas asinchroniškai. Paprasčiausias sprendimas – Azure Functions vykdyti pačiam robotui; Azure Storage Queue buvo naudojamas siųsti užduotis; Azure Storage Tables ir Azure Blob Storage saugojo rezultatus. Visi servisai sklandžiai integravosi su Azure Functions – sukurta SDK automatiškai užkraudavo/išsaugodavo duomenis.

Galiausiai turėjome pagalbines programėles (ir kartais naudojome azure emuliaciją), kurių dėka galėjome vykdyti ir trasuoti tikrintojus lokaliai. Žinoma neužmiršome ir didelio kiekio unit testų – jie tikrino įvairius teisingus bei neteisingus sprendinius.

Penkiolika tūkstančių pirmojo Angies sezono naudotojų sukūrė teisingų sprendimų, kokių nesitikėjom, arba labai išradingai bandė apeiti tikrintoją rašydami kaip galima mažiau kodo (su neteisingais sprendimais). Bet Mariaus sukurta infrastruktūra pasirodė labai atspari net ir ypatingam išradingumui, o prireikus infrastruktūrą saugiai keitėme ir vystėme.

Populiariausi blog'ai