Rumah  >  Artikel  >  hujung hadapan web  >  Cara Mengelakkan Perangkap Berulir Tunggal dalam JavaScript

Cara Mengelakkan Perangkap Berulir Tunggal dalam JavaScript

Patricia Arquette
Patricia Arquetteasal
2024-11-02 17:10:02821semak imbas

How to Avoid the Single-Threaded Trap in JavaScript

JavaScript sering digambarkan sebagai benang tunggal, yang bermaksud ia melaksanakan satu tugas pada satu masa. Tetapi adakah ini membayangkan bahawa setiap bahagian kod berjalan dalam pengasingan sepenuhnya, tanpa keupayaan untuk mengendalikan tugas lain sambil menunggu operasi tak segerak seperti respons HTTP atau permintaan pangkalan data? Jawapannya tidak! Malah, gelung acara dan janji JavaScript membenarkannya mengendalikan tugas tak segerak dengan cekap manakala kod lain terus dijalankan.

Sebenarnya javascript memang satu benang, walau bagaimanapun, salah faham bagaimana ini berfungsi boleh membawa kepada perangkap biasa. Satu perangkap sedemikian ialah mengurus operasi tak segerak seperti permintaan API, terutamanya apabila cuba mengawal akses kepada sumber yang dikongsi tanpa menyebabkan keadaan perlumbaan. Mari kita terokai contoh dunia sebenar dan lihat bagaimana pelaksanaan yang lemah boleh membawa kepada pepijat yang serius.

Saya menemui pepijat dalam aplikasi yang memerlukan log masuk ke perkhidmatan hujung belakang untuk mengemas kini data. Selepas log masuk, apl akan menerima token akses dengan tarikh tamat tempoh yang ditentukan. Setelah tarikh tamat tempoh ini berlalu, kami perlu mengesahkan semula sebelum membuat sebarang permintaan baharu pada titik akhir kemas kini. Cabaran timbul kerana titik akhir log masuk telah dikurangkan kepada maksimum satu permintaan setiap lima minit, manakala titik akhir kemas kini perlu dipanggil dengan lebih kerap dalam tetingkap lima minit yang sama. Adalah penting untuk logik berfungsi dengan betul, namun titik akhir log masuk kadang-kadang dicetuskan beberapa kali dalam selang lima minit, menyebabkan titik akhir kemas kini gagal berfungsi. Walaupun ada kalanya segala-galanya berfungsi seperti yang dijangkakan, pepijat yang terputus-putus ini menimbulkan risiko yang lebih serius, kerana ia boleh memberikan rasa selamat yang salah pada mulanya, menjadikannya kelihatan seperti sistem beroperasi dengan betul._

Untuk menggambarkan contoh ini, kami menggunakan apl NestJS yang sangat asas yang merangkumi perkhidmatan berikut:

  • AppService: Bertindak sebagai pengawal untuk mensimulasikan dua varian— versi buruk, yang kadangkala berfungsi dan kadangkala tidak, dan versi yang baik, yang dijamin sentiasa berfungsi dengan betul.
  • BadAuthenticationService: Pelaksanaan untuk versi buruk.
  • GoodAuthenticationService: Pelaksanaan untuk versi yang baik.
  • AbstractAuthenticationService: Kelas yang bertanggungjawab untuk mengekalkan keadaan kongsi antara GoodAuthenticationService dan BadAuthenticationService.
  • LoginThrottleService: Kelas yang menyerupai mekanisme pendikit titik akhir log masuk untuk perkhidmatan hujung belakang.
  • MockHttpService: Kelas yang membantu mensimulasikan permintaan HTTP.
  • MockAwsCloudwatchApiService: Mensimulasikan panggilan API ke sistem pengelogan AWS CloudWatch.

Saya tidak akan menunjukkan kod untuk semua kelas ini di sini; anda boleh menemuinya terus dalam repositori GitHub. Sebaliknya, saya akan memberi tumpuan khusus pada logik dan perkara yang perlu diubah agar ia berfungsi dengan betul.

Pendekatan Buruk:

@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;
    }
  }
}

Mengapa Ini Adalah Pendekatan Buruk:

Dalam BadAuthenticationService, kaedah loginToBackendService menetapkan this.loginInProgress kepada benar apabila memulakan permintaan log masuk. Walau bagaimanapun, kerana kaedah ini tidak segerak, ia tidak menjamin bahawa status log masuk akan dikemas kini dengan serta-merta. Ini boleh membawa kepada berbilang panggilan serentak ke titik akhir log masuk dalam had pendikit.
Apabila sendProtectedRequest mengesan bahawa token akses tiada, ia menyemak sama ada log masuk sedang dijalankan. Jika ya, fungsi menunggu sebentar dan kemudian cuba semula. Walau bagaimanapun, jika permintaan lain masuk pada masa ini, ia boleh mencetuskan percubaan log masuk tambahan. Ini boleh membawa kepada berbilang panggilan ke titik akhir log masuk, yang dikurangkan untuk membenarkan hanya satu panggilan setiap minit. Akibatnya, titik akhir kemas kini mungkin gagal seketika, menyebabkan tingkah laku yang tidak dapat diramalkan dan rasa selamat yang salah apabila sistem kelihatan berfungsi dengan baik pada masa-masa tertentu.

Ringkasnya, masalahnya terletak pada pengendalian operasi tak segerak yang tidak betul, yang membawa kepada keadaan perlumbaan berpotensi yang boleh memecahkan logik aplikasi.

Pendekatan yang baik:

@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;
    }
  }
}

Mengapa Ini Adalah Pendekatan yang Baik:

Dalam GoodAuthenticationService, kaedah loginToBackendService distrukturkan untuk mengendalikan logik log masuk dengan cekap. Peningkatan utama ialah pengurusan bendera loginInProgress. Ia ditetapkan selepas mengesahkan bahawa token akses tiada dan sebelum sebarang operasi tak segerak bermula. Ini memastikan bahawa sebaik sahaja percubaan log masuk dimulakan, tiada panggilan log masuk lain boleh dibuat secara serentak, dengan berkesan menghalang berbilang permintaan ke titik akhir log masuk pendikit.

Arahan Demo

Klon Repositori:

@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;
    }
  }
}

Pasang Ketergantungan yang Diperlukan:

@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;
    }
  }
}

Jalankan Aplikasi:

git clone https://github.com/zenstok/nestjs-singlethread-trap.git

Mensimulasikan permintaan:

  • Untuk mensimulasikan dua permintaan dengan versi buruk, hubungi:
cd nestjs-singlethread-trap
npm install

Untuk mensimulasikan dua permintaan dengan versi yang baik, hubungi:

npm run start

Kesimpulan: Mengelakkan Perangkap Satu Threaded JavaScript

Walaupun JavaScript adalah satu benang, ia boleh mengendalikan tugas tak segerak seperti permintaan HTTP dengan cekap menggunakan janji dan gelung acara. Walau bagaimanapun, pengendalian janji ini yang tidak betul, terutamanya dalam senario yang melibatkan sumber dikongsi (seperti token), boleh membawa kepada keadaan perlumbaan dan tindakan pendua.
Perkara utama ialah menyegerakkan tindakan tak segerak seperti log masuk untuk mengelakkan perangkap sedemikian. Sentiasa pastikan kod anda mengetahui proses yang sedang berjalan dan mengendalikan permintaan dengan cara yang menjamin penjujukan yang betul, walaupun semasa JavaScript melakukan pelbagai tugas di belakang tabir.

Jika anda belum menyertai Kelab Arnab Byte, kini adalah peluang anda untuk memasuki komuniti peminat perisian, pengasas teknologi dan pengasas bukan teknologi yang berkembang maju. Bersama-sama, kami berkongsi pengetahuan, belajar daripada satu sama lain, dan bersedia untuk membina permulaan besar seterusnya. Sertai kami hari ini dan jadilah sebahagian daripada perjalanan yang menarik ke arah inovasi dan pertumbuhan!

Atas ialah kandungan terperinci Cara Mengelakkan Perangkap Berulir Tunggal dalam JavaScript. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn