Maison > Article > interface Web > Comment éviter le piège du monothread en 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 :
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; } } }
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.
@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; } } }
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é.
@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; } } }
@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; } } }
git clone https://github.com/zenstok/nestjs-singlethread-trap.git
cd nestjs-singlethread-trap npm install
Pour simuler deux requêtes avec la bonne version, appelez :
npm run start
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!