التوثيق وحالات الاختبار: أدوات قوية لحل مشكلات تطوير OpenResty

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

بعد تعلم مبادئ وبعض المفاهيم الأساسية لـ OpenResty، سنبدأ أخيرًا في تعلم واجهة برمجة التطبيقات (API).

من تجربتي الشخصية، تعلم واجهة برمجة التطبيقات لـ OpenResty يعتبر سهلًا نسبيًا، لذا لن يتطلب الأمر العديد من المقالات لتقديمه. قد تتساءل: أليست واجهة برمجة التطبيقات هي الجزء الأكثر شيوعًا وأهمية؟ لماذا لا نقضي الكثير من الوقت عليها؟ هناك اعتباران رئيسيان.

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

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

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

لكن لا تقلق. يمكنك العثور على معظم هذه المحتويات في مجموعة حالات الاختبار.

بالنسبة لمطوري OpenResty، أفضل مواد تعلم واجهة برمجة التطبيقات هي الوثائق الرسمية وحالات الاختبار، وهي مهنية وصديقة للقارئ.

"أعطِ رجلاً سمكة، تطعمه ليوم واحد؛ علم رجلاً صيد السمك، تطعمه مدى الحياة." دعونا نستخدم مثالًا حقيقيًا لتجربة كيفية استغلال قوة الوثائق ومجموعة حالات الاختبار في تطوير OpenResty.

لنأخذ واجهة برمجة التطبيقات get لـ shdict كمثال

بناءً على منطقة الذاكرة المشتركة لـ NGINX، فإن القاموس المشترك (shared dictionary) هو كائن قاموس Lua، يمكنه الوصول إلى البيانات عبر عدة عمال وتخزين بيانات مثل تحديد المعدل، التخزين المؤقت، إلخ. هناك أكثر من 20 واجهة برمجة تطبيقات متعلقة بالقواميس المشتركة - وهي أكثر واجهات برمجة التطبيقات استخدامًا وأهمية في OpenResty.

لنأخذ أبسط عملية get كمثال؛ يمكنك النقر على رابط الوثائق للمقارنة. مثال الكود التالي هو نسخة مصغرة مأخوذة من الوثائق الرسمية.

http {
      lua_shared_dict dogs 10m;
      server {
          location /demo {
              content_by_lua_block {
                  local dogs = ngx.shared.dogs
                  dogs:set("Jim", 8)
                  local v = dogs:get("Jim")
                  ngx.say(v)
              }
          }
      }
  }

كملاحظة سريعة، قبل أن نتمكن من استخدام القاموس المشترك في كود Lua، نحتاج إلى إضافة كتلة ذاكرة في nginx.conf باستخدام توجيه lua_shared_dict، والذي يتم تسميته "dogs" وحجمه 10 ميجابايت. بعد تعديل nginx.conf، تحتاج إلى إعادة تشغيل العملية والوصول إليها باستخدام متصفح أو أمر curl لرؤية النتائج.

ألا يبدو هذا مرهقًا بعض الشيء؟ دعونا نعدله بشكل أكثر بساطة. كما ترى، استخدام واجهة سطر الأوامر resty بهذه الطريقة له نفس تأثير تضمين الكود في nginx.conf.

$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
 dogs:set("Jim", 8)
 local v = dogs:get("Jim")
 ngx.say(v)
 '

أنت الآن تعرف كيف يعمل nginx.conf وكود Lua معًا، وقد نجحت في تشغيل طرق set و get للقاموس المشترك. بشكل عام، يتوقف معظم المطورين عند هذا الحد. هناك بعض الأشياء التي تستحق الملاحظة هنا.

  1. أي مراحل لا يمكن استخدام واجهات برمجة التطبيقات المتعلقة بالذاكرة المشتركة؟
  2. نرى في كود المثال أن دالة get لها قيمة إرجاع واحدة فقط. متى سيكون هناك أكثر من قيمة إرجاع؟
  3. ما هو نوع الإدخال لدالة get؟ هل هناك حد للطول؟

لا تقلل من شأن هذه الأسئلة؛ يمكن أن تساعدنا على فهم OpenResty بشكل أفضل، وسأأخذك خلالها واحدة تلو الأخرى.

السؤال 1: أي مراحل لا يمكن استخدام واجهات برمجة التطبيقات المتعلقة بالذاكرة المشتركة؟

لننظر إلى السؤال الأول. الإجابة بسيطة؛ الوثائق لديها قسم context (أي قسم السياق) يوضح البيئات التي يمكن استخدام واجهة برمجة التطبيقات فيها.

context: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

كما ترى، لا يتم تضمين مراحل init و init_worker، مما يعني أن واجهة برمجة التطبيقات get للذاكرة المشتركة لا يمكن استخدامها في هاتين المرحلتين. يرجى ملاحظة أن كل واجهة برمجة تطبيقات للذاكرة المشتركة يمكن استخدامها في مراحل مختلفة. على سبيل المثال، يمكن استخدام واجهة برمجة التطبيقات set في مرحلة init.

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

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

كيف يمكننا العثور على مجموعة حالات الاختبار المتعلقة بالذاكرة المشتركة؟ حالات اختبار OpenResty كلها موجودة في الدليل /t ويتم تسميتها بشكل منتظم، أي self-incremented-number-function-name.t. ابحث عن shdict، وستجد 043-shdict.t، وهي مجموعة حالات اختبار الذاكرة المشتركة، والتي تحتوي على ما يقرب من 100 حالة اختبار، بما في ذلك اختبارات لظروف مختلفة طبيعية وغير طبيعية.

لنجرب تعديل أول حالة اختبار.

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

 === TEST 1: string key, int value
     --- http_config
         lua_shared_dict dogs 1m;
     --- config
         location = /test {
             init_by_lua '
                 local dogs = ngx.shared.dogs
                 local val = dogs:get("foo")
                 ngx.say(val)
             ';
         }
     --- request
     GET /test
     --- response_body
     32
     --- no_error_log
     [error]
     --- ONLY

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

بعد التعديل، يمكننا تشغيل حالة الاختبار باستخدام أمر prove.

prove t/043-shdict.t

ثم، ستظهر لك رسالة خطأ تؤكد قيود المرحلة الموضحة في الوثائق.

nginx: [emerg] "init_by_lua" directive is not allowed here

السؤال 2: متى يكون لدالة get أكثر من قيمة إرجاع؟

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

value, flags = ngx.shared.DICT:get(key)

في الظروف العادية.

  • المعلمة الأولى value تُرجع القيمة المقابلة للمفتاح key في القاموس؛ ومع ذلك، عندما لا يكون المفتاح موجودًا أو انتهت صلاحيته، تكون قيمة value هي nil.
  • المعلمة الثانية، flags، أكثر تعقيدًا بعض الشيء؛ إذا كانت واجهة set قد حددت flags، فإنها تُرجعها. وإلا، فلا.

إذا حدث خطأ في استدعاء واجهة برمجة التطبيقات، فإن value تُرجع nil، و flags تُرجع رسالة خطأ محددة.

من المعلومات الموجزة في الوثائق، يمكننا أن نرى أن local v = dogs:get("Jim") مكتوبة بمعلمة استقبال واحدة فقط. هذا النوع من الكتابة غير مكتمل لأنه يغطي فقط سيناريو الاستخدام النموذجي دون استقبال معلمة ثانية أو إجراء معالجة الاستثناءات. يمكننا تعديلها إلى ما يلي.

local data, err = dogs:get("Jim")
if data == nil and err then
    ngx.say("get not ok: ", err)
    return
end

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

  === TEST 65: get nil key
     --- http_config
         lua_shared_dict dogs 1m;
     --- config
         location = /test {
             content_by_lua '
                 local dogs = ngx.shared.dogs
                 local ok, err = dogs:get(nil)
                 if not ok then
                     ngx.say("not ok: ", err)
                     return
                 end
                 ngx.say("ok")
             ';
         }
     --- request
     GET /test
     --- response_body
     not ok: nil key
     --- no_error_log
     [error]

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

السؤال 3: ما هو نوع الإدخال لدالة get؟

أما بالنسبة للسؤال الثالث، ما هي أنواع معاملات الإدخال التي يمكن أن تكون لـ get؟ دعونا نتحقق من الوثائق أولاً، ولكن للأسف، ستجد أن الوثائق لا تحدد أنواع المفاتيح القانونية. ماذا نفعل؟

لا تقلق. على الأقل نعرف أن key يمكن أن يكون من نوع سلسلة ولا يمكن أن يكون nil. هل تتذكر أنواع البيانات في Lua؟ بالإضافة إلى السلاسل و nil، هناك الأرقام، المصفوفات، الأنواع المنطقية، والوظائف. النوعان الأخيران غير ضروريين كمفاتيح، لذا نحتاج فقط إلى التحقق من الأولين: الأرقام والمصفوفات. يمكننا البدء بالبحث في ملف الاختبار عن الحالات التي يتم فيها استخدام الأرقام كـ key.

=== TEST 4: number keys, string values

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

$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
 dogs:get({})
 '

كما هو متوقع، ظهر الخطأ التالي.

ERROR: (command line -e):2: bad argument #1 to 'get' (string expected, got table)

باختصار، يمكننا أن نستنتج أن أنواع key المقبولة من قبل واجهة برمجة التطبيقات get هي السلاسل والأرقام.

هل هناك حد لطول المفتاح الذي يتم تمريره؟ هناك حالة اختبار مقابلة هنا.

=== TEST 67: get a too-long key
     --- http_config
         lua_shared_dict dogs 1m;
     --- config
         location = /test {
             content_by_lua '
                 local dogs = ngx.shared.dogs
                 local ok, err = dogs:get(string.rep("a", 65536))
                 if not ok then
                     ngx.say("not ok: ", err)
                     return
                 end
                 ngx.say("ok")
             ';
         }
     --- request
     GET /test
     --- response_body
     not ok: key too long
     --- no_error_log
     [error]

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

الخلاصة

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

لذا، إذا واجهت مشكلة عند كتابة كود OpenResty، ما هي الطريقة المعتادة لحلها؟ هل هي الوثائق، قوائم البريد، أو قنوات أخرى؟

مرحبًا بمشاركة هذه المقالة مع زملائك وأصدقائك حتى نتمكن من التواصل والتحسين.