إعادة محاولة فشل طلبات HTTP في الزاوي

تنظيم الوصول إلى بيانات الخادم هو أساس أي تطبيق من صفحة واحدة تقريبًا. يتم تنزيل جميع المحتويات الديناميكية في هذه التطبيقات من الخلفية.

في معظم الحالات ، تعمل طلبات HTTP إلى الخادم بشكل موثوق وإرجاع النتيجة المرجوة. ومع ذلك ، في بعض الحالات ، قد تفشل الطلبات.

تخيل كيف يعمل شخص ما مع موقع الويب الخاص بك من خلال نقطة وصول في قطار يسافر في جميع أنحاء البلاد بسرعة 200 كيلومتر في الساعة. يمكن أن يكون اتصال الشبكة في هذا السيناريو بطيئًا ، ولكن طلبات الخادم ، على الرغم من ذلك ، تؤدي وظيفتها.

لكن ماذا لو دخل القطار إلى النفق؟ هناك احتمال كبير أن يتم قطع الاتصال بالإنترنت ولن يتمكن تطبيق الويب من "الوصول" إلى الخادم. في هذه الحالة ، سيتعين على المستخدم إعادة تحميل صفحة التطبيق بعد مغادرة القطار للنفق واستعادة اتصال الإنترنت.

يمكن أن تؤثر إعادة تحميل الصفحة على الحالة الحالية للتطبيق. هذا يعني أنه يمكن للمستخدم ، على سبيل المثال ، فقد البيانات التي أدخلها في النموذج.

بدلاً من مجرد التوفيق مع حقيقة أن طلبًا معينًا لم ينجح ، سيكون من الأفضل تكراره عدة مرات وإظهار إشعار إعلام للمستخدم. من خلال هذا النهج ، عندما يدرك المستخدم أن التطبيق يحاول مواجهة المشكلة ، فمن المرجح أنه لن يعيد تحميل الصفحة.



المادة ، التي نُنشر اليوم ترجمة لها ، مكرسة لتحليل عدة طرق لتكرار الاستعلامات غير الناجحة في التطبيقات الزاوية.

كرر الطلبات الفاشلة


دعونا نعيد إنتاج موقف قد يواجهه مستخدم يعمل على الإنترنت من قطار. سننشئ خلفية تعالج الطلب بشكل غير صحيح خلال أول ثلاث محاولات للوصول إليه ، مع إعادة البيانات فقط من المحاولة الرابعة.
عادة ، باستخدام Angular ، نقوم بإنشاء خدمة ، وربط HttpClient واستخدامها للحصول على البيانات من الخلفية.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      catchError(() => {        //           return EMPTY;      })    );  } } 

لا يوجد شيء خاص هنا. نحن سد العجز في وحدة الزاوي HttpClient وتنفيذ طلب بسيط الحصول على. إذا قام الطلب بإرجاع خطأ ، فنحن ننفذ بعض الأكواد Observable وإرجاع ملاحظة فارغة (كائن يمكن ملاحظته) Observable بما بدأ الطلب. هذا الرمز ، كما كان ، يقول: "كان هناك خطأ ، ولكن كل شيء في محله ، يمكنني التعامل معه."

معظم التطبيقات تؤدي طلبات HTTP بهذه الطريقة. في الرمز أعلاه ، يتم تنفيذ الطلب مرة واحدة فقط. بعد ذلك ، تقوم إما بإرجاع البيانات المستلمة من الخادم ، أو أنها غير ناجحة.

كيفية تكرار الطلب في حالة عدم توفر نقطة النهاية /greet أو إرجاع خطأ؟ ربما هناك بيان RxJS مناسب؟ بالطبع كان موجودا. RxJS لديها مشغلين لكل شيء.

أول ما قد يتبادر إلى الذهن في هذه الحالة هو retry . دعونا نلقي نظرة على تعريفه: "إرجاع ملاحظة يمكن تشغيلها في الملاحظة الأصلية باستثناء error . إذا كان error مكالمات الملاحظة الأصلية ، فإن هذه الطريقة ، بدلاً من نشر الخطأ ، ستعيد الاشتراك في الملاحظة الأصلية.

الحد الأقصى لعدد عمليات إعادة الاشتراك مقصور على count (هذه هي المعلمة العددية التي تم تمريرها إلى الطريقة). "

retry مشابه جدًا لما نحتاج إليه. لذلك دعونا تضمينها في سلسلة لدينا.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      retry(3),      catchError(() => {        //           return EMPTY;      }),      shareReplay()    );  } } 

لقد استخدمنا بنجاح مشغل retry . دعونا نلقي نظرة على كيفية تأثير ذلك على سلوك طلب HTTP الذي يتم تنفيذه في التطبيق التجريبي. فيما يلي ملف GIF كبير يعرض شاشة هذا التطبيق وعلامة التبويب الشبكة لأدوات مطور المتصفح. ستجد المزيد من هذه المظاهرات هنا.

طلبنا بسيط للغاية. يجعل فقط طلب HTTP عند النقر فوق الزر PING THE SERVER .

كما ذكرنا سابقًا ، تُرجع الواجهة الخلفية خطأً عند تنفيذ المحاولات الثلاثة الأولى لتنفيذ طلب إليه ، وعندما يصل إليها طلب رابع ، فإنها تُرجع استجابة طبيعية.

في علامة تبويب أداة مطور الشبكة ، يمكنك أن ترى أن retry تحل المهمة المعينة لها وتكرر تنفيذ الطلب الفاشل ثلاث مرات. آخر محاولة ناجحة ، يتلقى التطبيق استجابة ، تظهر رسالة مقابلة على الصفحة.

كل هذا جيد جدا. الآن يمكن للتطبيق تكرار الطلبات الفاشلة.

ومع ذلك ، لا يزال من الممكن تحسين هذا المثال. يرجى ملاحظة أنه يتم الآن تنفيذ الطلبات المتكررة فور تنفيذ الطلبات غير الناجحة. لن يحقق هذا السلوك للنظام فائدة كبيرة في وضعنا - عندما يدخل القطار إلى النفق ويفقد اتصال الإنترنت لفترة من الوقت.

تأخر إعادة محاولة الطلبات الفاشلة


القطار الذي دخل النفق لا يتركه على الفور. يقضي بعض الوقت هناك. لذلك ، نحتاج إلى "تمديد" الفترة التي نؤدي خلالها الطلبات المتكررة إلى الخادم. يمكنك القيام بذلك عن طريق تأجيل المحاولات.

للقيام بذلك ، نحتاج إلى التحكم بشكل أفضل في عملية تنفيذ الطلبات المتكررة. نحن بحاجة إلى أن نكون قادرين على اتخاذ قرارات بشأن متى بالضبط لتكرار الطلبات. هذا يعني أن قدرات مشغل retry لم تعد كافية بالنسبة لنا. لذلك ، ننتقل مرة أخرى إلى الوثائق على RxJS.

تحتوي الوثائق على وصف retryWhen ، والذي يبدو أنه retryWhen . في الوثائق ، يتم وصفها على النحو التالي: "إرجاع ملاحظ يقوم بتشغيل الملاحظة الأصلية باستثناء error . إذا حدث error مكالمات الملاحظة القابلة للملاحظة ، فستقوم هذه الطريقة بإلقاء "Throwable" ، والتي تسببت في حدوث الخطأ. إذا كانت مكالمات الملاحظة هذه complete أو error ، فستتصل هذه الطريقة complete أو error في الاشتراك الفرعي. وإلا ، فستعيد هذه الطريقة الاشتراك في الملاحظة الأصلية. "

نعم ، التعريف ليس بسيطًا. دعونا تصف نفسه في لغة أكثر سهولة.

تقبل retryWhen رد اتصال يقوم بإرجاع ملحوظ. يقرر " retryWhen " كيفية retryWhen عامل retryWhen عند بناءً على بعض القواعد. هذه هي الطريقة التي retryWhen المشغل:

  • توقف عن العمل ويلقي خطأ إذا كان المرتجعات المرتجعة يلقي خطأ.
  • يخرج إذا كانت التقارير المرتجعة المرتجعة قد أكملت.
  • في حالات أخرى ، عندما يعود الملاحظ بنجاح ، فإنه يكرر تنفيذ الملاحظة الأصلية

يتم استدعاء رد الاتصال فقط عندما يلقي Observable الأصلي خطأ لأول مرة.

الآن يمكننا استخدام هذه المعرفة لإنشاء آلية إعادة محاولة مؤجلة لطلب فاشل باستخدام عبارة retryWhen لإعادة retryWhen .

 retryWhen((errors: Observable<any>) => errors.pipe(    delay(delayMs),    mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry))    )) ) 

إذا كانت الملاحظة الأصلية ، والتي هي طلب HTTP الخاص بنا ، تُرجع خطأً ، فسيتم retryWhen عبارة " retryWhen . في رد الاتصال ، لدينا الوصول إلى الخطأ الذي تسبب في الفشل. نقوم بتأجيل errors ، وتقليل عدد مرات إعادة المحاولة ، وإرجاع ملاحظة جديدة يلقي خطأ.

استنادًا إلى قواعد retryWhen ، يقوم هذا " retryWhen ، لأنه retryWhen ، retryWhen الطلب. إذا لم ينجح التكرار عدة مرات وتقل قيمة متغير retries إلى 0 ، فسننهي المهمة بخطأ حدث أثناء تنفيذ الطلب.

! رائع على ما يبدو ، يمكننا أن نأخذ الكود أعلاه retry مشغل retry في سلسلتنا به. ولكن هنا نحن تبطئ قليلا.

كيفية retries مع retries المتغيرة؟ يحتوي هذا المتغير على الحالة الحالية لنظام إعادة محاولة الطلب الفاشلة. أين أعلنت؟ متى يتم إعادة ضبط الحالة؟ تحتاج الدولة إلى إدارتها داخل الدفق ، وليس خارجها.

statement إنشاء بيان delayedRetry الخاص بك


يمكننا حل مشكلة إدارة الحالة وتحسين قابلية قراءة الكود من خلال كتابة الكود أعلاه كمشغل RxJS منفصل.

هناك طرق مختلفة لإنشاء مشغلي RxJS الخاصين بك. تعتمد طريقة الاستخدام على كيفية تنظيم المشغل المعين.

يعتمد المشغل لدينا على مشغلي RxJS الحاليين. نتيجة لذلك ، يمكننا استخدام أبسط طريقة لإنشاء مشغلين خاصين بنا. في حالتنا ، فإن عامل RxJ هو مجرد وظيفة بالتوقيع التالي:

 const customOperator = (src: Observable<A>) => Observable<B> 

هذا البيان يأخذ ملاحظته الأصلية ويعيد ملاحظه أخرى.

نظرًا لأن المشغل الخاص بنا يسمح للمستخدم بتحديد عدد مرات تنفيذ الطلبات المتكررة ، وعدد المرات التي يحتاجون فيها إلى التنفيذ ، نحتاج إلى التفاف إعلان الوظيفة أعلاه في وظيفة المصنع ، والتي تأخذ delayMs (التأخير بين maxRetry ) و maxRetry ( الحد الأقصى لعدد التكرار).

 const customOperator = (delayMs: number, maxRetry: number) => {   return (src: Observable<A>) => Observable<B> } 

إذا كنت ترغب في إنشاء مشغل لا يعتمد على عوامل التشغيل الحالية ، فعليك الانتباه إلى معالجة الأخطاء والاشتراكات. علاوة على ذلك ، سوف تحتاج إلى تمديد فئة Observable وتنفيذ وظيفة lift .

إذا كنت مهتما ، نلقي نظرة هنا .

لذلك ، استنادًا إلى مقتطفات الشفرة أعلاه ، فلنكتب مشغل RxJs الخاص بنا.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        delay(delayMs),        mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry))        ))      )    ); } 

ممتاز. الآن يمكننا استيراد هذا المشغل إلى رمز العميل. سنستخدمه عند تنفيذ طلب HTTP.

 return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(        delayedRetry(1000, 3),        catchError(error => {            console.log(error);            //               return EMPTY;        }),        shareReplay()    ); 

نضع عامل التشغيل delayedRetry في السلسلة delayedRetry الأرقام 1000 و 3. كمعلمات delayedRetry الأولى تحدد التأخير بالمللي ثانية بين محاولات تقديم طلبات متكررة. تحدد المعلمة الثانية الحد الأقصى لعدد الطلبات المتكررة.

أعد تشغيل التطبيق وانظر كيف يعمل المشغل الجديد.

بعد تحليل سلوك البرنامج باستخدام أدوات مطور المتصفح ، يمكننا أن نرى أن تنفيذ المحاولات المتكررة لتنفيذ الطلب قد تأخر لثانية واحدة. بعد تلقي الإجابة الصحيحة على الطلب ، ستظهر رسالة مقابلة في نافذة التطبيق.

طلب الأسى قيلولة بعد الظهر


دعنا نطور فكرة التأخير في إعادة المحاولة للطلبات الفاشلة. في السابق ، كنا نتأخر دائمًا في تنفيذ كل طلب من الطلبات المتكررة في نفس الوقت.

هنا نتحدث عن كيفية زيادة التأخير بعد كل محاولة. يتم إجراء المحاولة الأولى لإعادة محاولة الطلب بعد ثانية ، والثانية بعد ثانيتين ، والثالثة بعد ثلاث.

قم بإنشاء عبارة جديدة ، retryWithBackoff ، والتي تنفذ هذا السلوك.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        mergeMap(error => {            if (retries-- > 0) {              const backoffTime = delayMs + (maxRetry - retries) * backoffMs;              return of(error).pipe(delay(backoffTime));            }            return throwError(getErrorMessage(maxRetry));          }        )))); } 

إذا كنت تستخدم هذا المشغل في التطبيق واختبرته ، يمكنك أن ترى كيف يزداد التأخير في تنفيذ الطلب المتكرر بعد كل محاولة جديدة.

بعد كل محاولة ، ننتظر وقتًا معينًا ، ونكرر الطلب ونزيد وقت الانتظار. هنا ، كالعادة ، بعد أن يعرض الخادم الإجابة الصحيحة على الطلب ، نعرض رسالة في نافذة التطبيق.

النتائج


تكرار طلبات HTTP الفاشلة يجعل التطبيقات أكثر استقرارًا. هذا مهم بشكل خاص عند إجراء استعلامات مهمة للغاية ، بدون البيانات التي تم الحصول عليها من خلالها ، لا يمكن للتطبيق العمل بشكل طبيعي. على سبيل المثال ، يمكن أن يكون بيانات التكوين التي تحتوي على عناوين الخوادم التي يحتاج التطبيق إلى التفاعل معها.

في معظم السيناريوهات ، لا retry إعادة محاولة RxJs كافية لتوفير نظام موثوق لإعادة المحاولة للطلبات الفاشلة. يعطي retryWhen عند المطور مستوى أعلى من التحكم في الطلبات المتكررة. يسمح لك بتكوين الفاصل الزمني للطلبات المتكررة. نظرًا لقدرات هذا المشغل ، من الممكن تنفيذ مخطط التكرار المتأخر أو التكرار المتأخر بشكل كبير.

عند تنفيذ أنماط السلوك المناسبة لإعادة الاستخدام في سلاسل RxJS ، يوصى بتنسيقها كمشغلين جدد. هذا هو المستودع الذي تم استخدام الكود منه في هذه المقالة.

أعزائي القراء! كيف يمكنك حل مشكلة إعادة محاولة طلبات HTTP الفاشلة؟

Source: https://habr.com/ru/post/ar459302/


All Articles