Don’t use Node(backend), unless you need asynchronous code

https://hackernoon.com/dont-use-node-as-a-server-unless-you-need-asynchronous-code-60f41729dbc1

TLDR version:

When programming servers in nodejs, callbacks add complexity and can be a source of confusion, even when you are careful and design your program well. I would only recommend using nodejs as a server if the asynchronous non-blocking design is a critical part of the requirements of your system.

Cautam ceva legat de node si am dat de articolul de mai sus. M-a atras titul, l-am citit, si-am zis sa cer pareri si de la oameni care au lucrat mai mult cu node decat mine. Prin urmare, ce ziceti, are dreptate?

4 Likes

Nu folosesc nodejs, dar în principiu sunt de acord cu ideea că asincronismul (event driven programming) şi firele de execuţie (multihreading-ul) n-ar trebuie să fi folosite decât dacă ai într-adevăr nevoie de ele. Altfel, nu faci decât să-ţi baţi singur cuie în talpă, debugging-ul unei aplicaţii al cărei flow merge pe fire multiple e un coşmar.


Stăteam acum şi-mi storceam mintea să imaginez un scenariu în care aş avea nevoie de asincronism la o aplicaţie web şi nu-mi vine niciunul :slight_smile:

Multithreading, da, dacă ai ceva complex de executat, îl împarţi în mai multe taskuri, le lansezi pe mai multe fire, firul principal aşteptând să termine toate, după care merge mai departe cu rezultatul.

Programarea pe evenimente e mai degrabă utilă la GUI-uri sau la servere, acolo unde de regula bucla principală stă cea mai mare parte din timp nefăcând nimic, iar task-urile care îi vin sunt foarte simple şi se execută cvasi-instantaneu (de exemplu să răspunzi la un buton sau să serveşti un fişier).

Eu sunt curios ce API-uri web nu le considerati event driven sau asynchron.

Serviciile web sunt asincron, chiar daca le scrii in php, doar codul tau ruleaza sincron, paradigma de HTTP (request / response) este asincron.

Cum este mentionat cu “callbacks add complexity” as putea spune ca si OOP in php “adds complexity”.

Eu personal nu prea ma lovesc de partea cu “omfg callbacks!!”. Conteaza mult din ce exemple inveti si cum structurezi codul. Nu prea folosesc clase, decat cand am nevoie sa creez un service care trebuie instantiat cu ceva optiuni, in rest functii care contin async / await.

import Server from "@nore/server"
import SQLite from "@nore/sqlite"
import schemas from "./schemas.js"

const server = new Server({ port: 3000, host: "localhost" });
const db = new SQLite({ file: "./db.sqlite" });
const accounts = db.table("accounts")

server.route({
	method: "POST",
	path: "/api/login",
	schema: { body: schemas.login },
	async handler(request, reply) {
		const data = request.body;
		const [account] = await accounts.find({ email: data.email });

		// check if the account exists
		if (!account) {
			throw reply.error("badRequest", "Invalid email or password");
		}

		// check if the password is valid
		const isValid = await accounts.isPasswordValid(
			data.password,
			account.password
		);

		if (!isValid) {
			throw reply.error("badRequest", "Invalid email or password");
		}

		// add the account to the session
		request.session.account = account;

		// response: { success: 1 }
		reply.success();
	},
});
5 Likes

XMLHttpRequest este singurul API care poate fi folosit syncron, dar NU este recomandat.

Păi doar partea de dispatching este asincronă, dispatcher-ul serverului HTTP preia clientul, îi aloca un worker (care de cele mai multe ori este un thread sau proces separat, care uneori deja rulează) şi iese, este pregătit pentru următorul request. Programatorul aplicaţiei nu trebuie să-şi bata capul cu chestia asta, e treaba serverului HTTP.

Asta în browser, pentru că în timp ce astepţi să vina datele, vrei să poţi folosi în continuare browserul. Pe server side… nu pare să aibă vreun sens. Porneşti operaţiunea asincronă, după care ce faci? Tot va trebui să aştepţi să termine, deci tot sincronă e. Are sens doar dacă ai nevoie să rulezi mai multe operaţiuni asincrone simultan, altfel nu face nicio diferenţă.

Probabil m-am obisnuit cu paradigma de event driven, mi se pare mult mai simplu sa gandesc asa fata de modelul OOP / procedural.

Din ce-am observat multi nu prea fac error handling, nu se diferentiaza intre operational errors si programming (human) errors.

Modelul de error handling este foarte similar cu conceptul event driven. Ai un flow normal de executie pe o serie de task-uri si cand ceva nu este valid faci throw la error. Sunt cazuri in care ai nevoie sa modifici flow-ul cand se intampla o eroare apoi sa dai un raspuns utilizatorului.

Ca exemplu:

Cum gandeam cand scriam php:

Cum gandesc de cand scriu node:


Node’s callback model, error first paradigm and how did we get here

.
Step 1: understand asynchrony brings time, and time flows forward:

Step 2: use a functional model approach: useCaseFunction(input) => output

Step 3: a task (use case) can succeed or fail

Pt. ca error handling-ul este important in node s-a promovat ideea de error first.

// when calling a callback pass the error as the first parameter
                         ↓↓↓↓↓ 	
getUserById(session.id, (error, user) => {
  if (error) {
    // failure
  }
  else {
    // success 
  }
})

Callback-urile au fost folosite pt. ca in 2010 nu exista o alta sintaxa prin care se putea folosi modelul asincron. Callback hell a aparut pt. ca majoritatea au facut un nesting prea deep la cod:

onRequest(({ request, session }, reply) => {
  getUserById(session.id, (error, user) => {
    if (error) {
      // failure
    }
    else {
      sendPasswordResetEmail(user.email, (error) => {
        if (error) {
          // failure
        }
        else {
          logUserActionToAnalytics({ ... }, (error) => {
            if (error) {
              // failure
            }
            else {
              // success
            }
          })
        }
      })
    }
  })
})

Acum exista async / await + promises care fac codul sa arate mult mai clean.

async function onRequest({ request, reply, session }) {
  const user = await getUserById(session.id)

  if (!user) {
    throw new OperationalError("User was not found")
  }

  await sendPasswordResetEmail(user.email)
  await logUserActionToAnalytics({ ... })

  reply({ success: 1 })
}

// where onRequest is called is wrapped in a try / catch block
12 Likes

Un exemplu simplu ar fi sa zicem ca ceri in avans datele. De exemplu ai o pagina unde vrei sa validezi ca un utilizator poate sa vizualizeze un anumit produs, tu poti sa pornesti in paralel cererile si atunci ar fi ceva de genul:

Serializat:

  1. Cer informatii despre user
  2. Validez ca user-ul are acces
  3. Cer informatii despre produs
  4. Afisezi datele

Paralel:

  1. Cer informatii despre user si produs in paralel
  2. Validez ca user-ul are acces
  3. Afisezi datele

Poti sa obtii o performanta mai buna in acest caz (ca si overhead operational esti tot acolo), astfel incat pagina se va incarca vizual de doua ori mai repede.

In cazul in care vrei sa procesezi datele in NodeJS te poti lovi de multe probleme, cazuri concrete intalnite de mine:

  1. Afisarea unui tabel (CSV export) care contine procesari de date multiple.
  2. Picture resize in procesul de NodeJS pentru un media server

Ambele ocupa CPU si event dispatcher-ul nu apuca sa dea drumul la un alt fir de executie blocat astfel incat te poti trezi ca serverul web nu raspunde la request-uri din cauza unor operatiuni CPU bound.
Cat timp faci procesari minime pe datele respective e ok.

1 Like

O alta problema cu Node este ca este single threaded si poti avea probleme maricele doar din cauza vreunui regex scris mai prost, care ar putea bloca toate requesturile.

Si totusi, avantajul de a avea acelasi limbaj pentru front-end si back-end e mai important decat toate neajunsurile, care pot fi evitate cu putina disciplina in cod. Plus, subscriu la ce a spus @navaru mai sus, o data ce te obisnuiesti cu event-driven architecture, totul este mult mai natural.

1 Like

De când să știi HTML, CSS ,SQL, Javascript și PHP (plus Python, Perl, Ruby, Go și C#) e așa o problemă?

2 Likes

Resource sharing, dezvolti aceeasi librarie/functionalitate pentru mai multe parti din stack.

1 Like

Ideea e că într-o aplicaţie web obişnuită nu prea ai evenimente care să-ţi vină asincron. Poate doar în scenariile în care execuţi două interogări SQL simultan, acel gen de prefetch descris de @pghoratiu.

Totuşi, senzaţia mea este că scenariul ăsta e mai simplu de implementat cu thread-uri. Lansezi cele două thread-uri şi pui mainthread-ul în wait până se termină cele secundare. Nu cunosc js prea bine, cum se pot sincroniza două callback-uri? Cred că mainloop-ul trebuie pus pe pauză până în momentul în care toate callback-urile au primit evenimentele cuvenite, dar nu-mi dau seama prin ce mecanism se poate face asta. Polling-ul nu pare a fi o idee prea bună, consumă timp de procesor.

Depinde de nevoile aplicatiei, daca ai nevoie de API-uri sa functioneze pe WebSockets si REST, mai nou GraphQL, se complica lucrurile daca sunt gandite sincron, mai ales daca ai business logic + logging + analytics + extra shit.

Partea cu extra shit este cand clientul iti vine cu vreo idee idioata care ar necesita ceva refactoring si nu se stie daca este temporara sau nu. Avand un set de evenimente, sistemul devine pluggable, te legi la ce evenimente ai nevoie neafectand flow-ul operational principal.

Ar fi fost interesant daca in JS existau light-threads ca in Go, dar din pacate nu sunt.

Daca esti familiar cu CSP, poti crea un sistem similar si sincronizezi prin channels, example.

1 Like

Aha, am înţeles. Am mai folosit metoda cu channel-uri (pentru comunicare între thread-uri, folosind pipe()), habar n-aveam că are şi un nume chestia asta :slight_smile:

În exemplul tău funcţia “printer()” poate fi cea care aşteaptă să se adune datele aşteptate, şi, când s-au adunat toate, ia decizia de execuţie finală şi exit. Un pic peste mână, dar merge.

În orice caz, e o metodă complicată şi error-prone, n-ar trebui folosită decât dacă nu ai încotro.

De exemplu, folosesc o aplicaţie care mă scoate din minţi (kmail). Pare să lanseze un soi de thread, care din diverse motive nu mai trimite niciodată feedback şi dispacher-ul rămâne agăţat, aşteptând la infinit thread-ul buclucaş. Cred că e o idee bună să existe setate timeout-uri pentru chestiile astea, ceea ce ridică şi mai mult nivelul de complexitate.

Eu lucrez cu multe servicii 3rd party si inevitabil folosesc timeout-uri si retry-uri. In practica nu e chiar atat de complicat daca ai codul bine structurat. Sunt unele servicii care au o limita de 4 request-uri pe secunda si uneori nu mai raspund, functionalitatea de try / delay / timeout e simpla:

const args = { ... }

const result = await tryRequest(getPaymentHistory, args, {
  retry: 3,  // retry 3 times
  timeout: "30s", // wait a maximum of 30s per try
  delay: "250ms", // wait 250 milliseconds before trying again
})
1 Like

Am folosit aync/await si in c#. La fel ca si in JS, iti permit sa lucrezi asincron. Api-urile .net au 2 variante, una sincrona si una asincrona prefixata cu ...aync. In combinatie cu atasarea rapida si fara bataie de cap a unui eveniment folosind operatorul + se pot scrie constructii de cod elegante si usor de citit :slight_smile:

Sa nu mai zic si de api-uri 3rd parties precum cel de la Github,

private DamageResult CalculateDamageDone()
{
    // Code omitted:
    //
    // Does an expensive calculation and returns
    // the result of that calculation.
}


calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work.  The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Este foarte simplu sa iti dai seama ce face codul de mai sus.

In java sa lucrezi asincron mi se pare mult mai dificil. Constructia este asemanatoare cu promise-ul din js.

Sper ca intr-o zi sa vadem o simplificare a api-ului :smiley:
Niste baieti destepti, au facut asta pt java. Cica merge pe acelasi principiu ca la C#

O chestie interesanta ce ar putea usura treba cu thread-uri, ar putea fi Fibers

2 Likes