Maison > interface Web > js tutoriel > le corps du texte

Comment éviter le piège du monothread en JavaScript

Patricia Arquette
Libérer: 2024-11-02 17:10:02
original
904 Les gens l'ont consulté

How to Avoid the Single-Threaded Trap in JavaScript

JavaScript est souvent décrit comme à thread unique, ce qui signifie qu'il exécute une tâche à la fois. Mais cela implique-t-il que chaque morceau de code s'exécute de manière totalement isolée, sans possibilité de gérer d'autres tâches en attendant des opérations asynchrones telles que des réponses HTTP ou des requêtes de base de données ? La réponse est non ! En fait, la boucle d'événements et les promesses de JavaScript lui permettent de gérer efficacement les tâches asynchrones pendant que d'autres codes continuent de s'exécuter.

La vérité est que javascript est en effet monothread, cependant, une mauvaise compréhension de son fonctionnement peut conduire à des pièges courants. L'un de ces pièges consiste à gérer des opérations asynchrones telles que les requêtes API, en particulier lorsque l'on tente de contrôler l'accès aux ressources partagées sans provoquer de conditions de concurrence. Explorons un exemple concret et voyons comment une mauvaise mise en œuvre peut entraîner de graves bugs.

J'ai rencontré un bug dans une application qui nécessitait de se connecter à un service backend pour mettre à jour les données. Lors de la connexion, l'application recevrait un jeton d'accès avec une date d'expiration spécifiée. Une fois cette date d'expiration passée, nous devions nous réauthentifier avant de faire toute nouvelle demande au point de terminaison de mise à jour. Le problème est survenu parce que le point de terminaison de connexion était limité à un maximum d’une requête toutes les cinq minutes, tandis que le point de terminaison de mise à jour devait être appelé plus fréquemment au cours de cette même fenêtre de cinq minutes. Il était essentiel que la logique fonctionne correctement, mais le point de terminaison de connexion était déclenché occasionnellement plusieurs fois dans un intervalle de cinq minutes, ce qui entraînait un dysfonctionnement du point de terminaison de mise à jour. Même s'il y avait des moments où tout fonctionnait comme prévu, ce bug intermittent présentait un risque plus grave, car il pouvait donner un faux sentiment de sécurité au début, donnant l'impression que le système fonctionnait correctement._

Pour illustrer cet exemple, nous utilisons une application NestJS très basique qui comprend les services suivants :

  • AppService : agit comme un contrôleur pour simuler deux variantes : la mauvaise version, qui fonctionne parfois et parfois non, et la bonne version, dont le fonctionnement est garanti toujours correctement.
  • BadAuthenticationService : Implémentation pour la mauvaise version.
  • GoodAuthenticationService : Implémentation pour la bonne version.
  • AbstractAuthenticationService : Classe responsable du maintien de l'état partagé entre GoodAuthenticationService et BadAuthenticationService.
  • LoginThrottleService : classe qui simule le mécanisme de limitation du point de terminaison de connexion pour le service backend.
  • MockHttpService : Classe qui permet de simuler les requêtes HTTP.
  • MockAwsCloudwatchApiService : simule un appel d'API au système de journalisation AWS CloudWatch.

Je ne montrerai pas le code de toutes ces classes ici ; vous pouvez le trouver directement dans le référentiel GitHub. Au lieu de cela, je me concentrerai spécifiquement sur la logique et sur ce qui doit être modifié pour qu'elle fonctionne correctement.

La mauvaise approche :

@Injectable()
export class BadAuthenticationService extends AbstractAuthenticationService {
  async loginToBackendService() {
    this.loginInProgress = true; // this is BAD, we are inside a promise, it's asynchronous. it's not synchronous, javascript can execute it whenever it wants

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com/login`, {
          password: 'password',
        }),
      );

      return response;
    } finally {
      this.loginInProgress = false;
    }
  }

  async sendProtectedRequest(route: string, data?: unknown) {
    if (!this.accessToken) {
      if (this.loginInProgress) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return this.sendProtectedRequest(route, data);
      }

      try {
        await this.awsCloudwatchApiService.logLoginCallAttempt();
        const { data: loginData } = await this.loginToBackendService();
        this.accessToken = loginData.accessToken;
      } catch (e: any) {
        console.error(e?.response?.data);
        throw e;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com${route}`, data, {
          headers: {
            Authorization: `Bearer ${this.accessToken}`,
          },
        }),
      );

      return response;
    } catch (e: any) {
      if (e?.response?.data?.statusCode === 401) {
        this.accessToken = null;
        return this.sendProtectedRequest(route, data);
      }
      console.error(e?.response?.data);
      throw e;
    }
  }
}
Copier après la connexion
Copier après la connexion

Pourquoi c'est une mauvaise approche :

Dans BadAuthenticationService, la méthode loginToBackendService définit this.loginInProgress sur true lors du lancement d'une demande de connexion. Cependant, comme cette méthode est asynchrone, elle ne garantit pas que le statut de connexion sera mis à jour immédiatement. Cela pourrait conduire à plusieurs appels simultanés au point de terminaison de connexion dans la limite de limitation.
Lorsque sendProtectedRequest détecte que le jeton d'accès est absent, il vérifie si une connexion est en cours. Si tel est le cas, la fonction attend une seconde puis réessaye. Cependant, si une autre demande arrive pendant cette période, elle peut déclencher des tentatives de connexion supplémentaires. Cela peut entraîner plusieurs appels vers le point de terminaison de connexion, qui est limité pour autoriser un seul appel par minute. En conséquence, le point de terminaison de mise à jour peut échouer par intermittence, provoquant un comportement imprévisible et un faux sentiment de sécurité lorsque le système semble parfois fonctionner correctement.

En résumé, le problème réside dans la mauvaise gestion des opérations asynchrones, ce qui conduit à de potentielles conditions de concurrence pouvant briser la logique de l'application.

La bonne approche :

@Injectable()
export class GoodAuthenticationService extends AbstractAuthenticationService {
  async loginToBackendService() {
    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com/login`, {
          password: 'password',
        }),
      );

      return response;
    } finally {
      this.loginInProgress = false;
    }
  }

  async sendProtectedRequest(route: string, data?: unknown) {
    if (!this.accessToken) {
      if (this.loginInProgress) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return this.sendProtectedRequest(route, data);
      }

      // Critical: Set the flag before ANY promise call
      this.loginInProgress = true;

      try {
        await this.awsCloudwatchApiService.logLoginCallAttempt();
        const { data: loginData } = await this.loginToBackendService();
        this.accessToken = loginData.accessToken;
      } catch (e: any) {
        console.error(e?.response?.data);
        throw e;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com${route}`, data, {
          headers: {
            Authorization: `Bearer ${this.accessToken}`,
          },
        }),
      );

      return response;
    } catch (e: any) {
      if (e?.response?.data?.statusCode === 401) {
        this.accessToken = null;
        return this.sendProtectedRequest(route, data);
      }
      console.error(e?.response?.data);
      throw e;
    }
  }
}
Copier après la connexion
Copier après la connexion

Pourquoi c'est une bonne approche :

Dans GoodAuthenticationService, la méthode loginToBackendService est structurée pour gérer efficacement la logique de connexion. La principale amélioration réside dans la gestion du flag loginInProgress. Il est défini après confirmation de l'absence d'un jeton d'accès et avant le début de toute opération asynchrone. Cela garantit qu'une fois la tentative de connexion lancée, aucun autre appel de connexion ne peut être effectué simultanément, empêchant ainsi plusieurs requêtes au point de terminaison de connexion limité.

Instructions de démonstration

Clonez le référentiel :

@Injectable()
export class BadAuthenticationService extends AbstractAuthenticationService {
  async loginToBackendService() {
    this.loginInProgress = true; // this is BAD, we are inside a promise, it's asynchronous. it's not synchronous, javascript can execute it whenever it wants

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com/login`, {
          password: 'password',
        }),
      );

      return response;
    } finally {
      this.loginInProgress = false;
    }
  }

  async sendProtectedRequest(route: string, data?: unknown) {
    if (!this.accessToken) {
      if (this.loginInProgress) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return this.sendProtectedRequest(route, data);
      }

      try {
        await this.awsCloudwatchApiService.logLoginCallAttempt();
        const { data: loginData } = await this.loginToBackendService();
        this.accessToken = loginData.accessToken;
      } catch (e: any) {
        console.error(e?.response?.data);
        throw e;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com${route}`, data, {
          headers: {
            Authorization: `Bearer ${this.accessToken}`,
          },
        }),
      );

      return response;
    } catch (e: any) {
      if (e?.response?.data?.statusCode === 401) {
        this.accessToken = null;
        return this.sendProtectedRequest(route, data);
      }
      console.error(e?.response?.data);
      throw e;
    }
  }
}
Copier après la connexion
Copier après la connexion

Installez les dépendances nécessaires :

@Injectable()
export class GoodAuthenticationService extends AbstractAuthenticationService {
  async loginToBackendService() {
    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com/login`, {
          password: 'password',
        }),
      );

      return response;
    } finally {
      this.loginInProgress = false;
    }
  }

  async sendProtectedRequest(route: string, data?: unknown) {
    if (!this.accessToken) {
      if (this.loginInProgress) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return this.sendProtectedRequest(route, data);
      }

      // Critical: Set the flag before ANY promise call
      this.loginInProgress = true;

      try {
        await this.awsCloudwatchApiService.logLoginCallAttempt();
        const { data: loginData } = await this.loginToBackendService();
        this.accessToken = loginData.accessToken;
      } catch (e: any) {
        console.error(e?.response?.data);
        throw e;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com${route}`, data, {
          headers: {
            Authorization: `Bearer ${this.accessToken}`,
          },
        }),
      );

      return response;
    } catch (e: any) {
      if (e?.response?.data?.statusCode === 401) {
        this.accessToken = null;
        return this.sendProtectedRequest(route, data);
      }
      console.error(e?.response?.data);
      throw e;
    }
  }
}
Copier après la connexion
Copier après la connexion

Exécutez l'application :

git clone https://github.com/zenstok/nestjs-singlethread-trap.git
Copier après la connexion

Simuler des requêtes :

  • Pour simuler deux requêtes avec la mauvaise version, appelez :
cd nestjs-singlethread-trap
npm install
Copier après la connexion

Pour simuler deux requêtes avec la bonne version, appelez :

npm run start
Copier après la connexion

Conclusion : éviter les pièges du monothread de JavaScript

Bien que JavaScript soit monothread, il peut gérer efficacement des tâches asynchrones telles que les requêtes HTTP en utilisant les promesses et la boucle d'événements. Cependant, une mauvaise gestion de ces promesses, en particulier dans les scénarios impliquant des ressources partagées (comme les jetons), peut conduire à des conditions de concurrence et à des actions en double.
L’essentiel à retenir est de synchroniser les actions asynchrones telles que les connexions pour éviter de tels pièges. Assurez-vous toujours que votre code est conscient des processus en cours et traite les demandes de manière à garantir un séquençage approprié, même lorsque JavaScript est multitâche en coulisses.

Si vous n'avez pas encore rejoint le Rabbit Byte Club, c'est maintenant votre chance de rejoindre une communauté florissante de passionnés de logiciels, de fondateurs technologiques et de fondateurs non technologiques. Ensemble, nous partageons nos connaissances, apprenons les uns des autres et nous préparons à créer la prochaine grande startup. Rejoignez-nous aujourd'hui et faites partie d'un voyage passionnant vers l'innovation et la croissance !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal