पिछले हफ़्ते, मैंने का लिखा था। विनिर्देश का उद्देश्य दोहराए गए अनुरोधों से बचना है। संक्षेप में, विचार यह है कि क्लाइंट अनुरोध के साथ एक अद्वितीय कुंजी भेजे: IETF Idempotency-Key विनिर्देश विश्लेषण यदि सर्वर को कुंजी का पता नहीं है, तो वह सामान्य रूप से आगे बढ़ता है और फिर प्रतिक्रिया को संग्रहीत करता है। यदि सर्वर को कुंजी पता है, तो वह आगे की प्रक्रिया को रोक देता है तथा संग्रहीत प्रतिक्रिया को तुरंत लौटा देता है। यह पोस्ट दिखाता है कि इसे के साथ कैसे कार्यान्वित किया जाए। Apache APISIX अवलोकन कोडिंग शुरू करने से पहले, हमें कुछ चीजें परिभाषित करने की आवश्यकता है। अपाचे APISIX एक प्लगइन-आधारित आर्किटेक्चर प्रदान करता है। इसलिए, हम उपरोक्त तर्क को एक प्लगइन में कोड करेंगे। अपाचे APISIX ओपनरेस्टी पर आधारित है, जो nginx पर आधारित है। प्रत्येक घटक चरणों को परिभाषित करता है, जो घटकों में कमोबेश मैप होते हैं। चरणों के बारे में अधिक जानकारी के लिए, कृपया देखें। यह पिछली पोस्ट अंत में, हम प्राथमिकता पर निर्णय लेंगे। प्राथमिकता उस क्रम को परिभाषित करती है जिसमें APISIX प्लगइन चलाता है। मैंने पर निर्णय लिया, क्योंकि सभी प्रमाणीकरण प्लगइन्स की प्राथमिकता और उससे अधिक रेंज में होती है, लेकिन मैं कैश्ड प्रतिक्रिया को जल्द से जल्द वापस करना चाहता हूँ। किसी चरण के अंदर 1500 2000 विनिर्देश के अनुसार हमें डेटा संग्रहीत करना आवश्यक है। APISIX कई अमूर्तताएं प्रदान करता है, लेकिन संग्रहण उनमें से एक नहीं है। हमें आइडेम्पोटेंसी कुंजी के माध्यम से एक्सेस की आवश्यकता है ताकि यह एक कुंजी-मूल्य संग्रह की तरह दिखे। मैंने मनमाने ढंग से Redis को चुना, क्योंकि यह काफी व्यापक है, क्लाइंट पहले से ही APISIX वितरण का हिस्सा है। ध्यान दें कि सरल Redis JSON संग्रहण प्रदान नहीं करता है; इसलिए, मैं Docker छवि का उपयोग करता हूँ। और redis-stack स्थानीय बुनियादी ढांचा निम्नलिखित है: services: apisix: image: apache/apisix:3.9.0-debian volumes: - ./apisix/config.yml:/usr/local/apisix/conf/config.yaml:ro - ./apisix/apisix.yml:/usr/local/apisix/conf/apisix.yaml:ro #1 - ./plugin/src:/opt/apisix/plugins:ro #2 ports: - "9080:9080" redis: image: redis/redis-stack:7.2.0-v9 ports: - "8001:8001" #3 स्थैतिक मार्ग विन्यास हमारे भविष्य के प्लगइन का मार्ग रेडिस इनसाइट्स (GUI) का पोर्ट। आवश्यक नहीं है, लेकिन डिबगिंग के लिए विकास के दौरान बहुत उपयोगी है। वैसे तो यह APISIX कॉन्फ़िगरेशन निम्नलिखित है: deployment: role: data_plane role_data_plane: config_provider: yaml #1 apisix: extra_lua_path: /opt/?.lua #2 plugins: - idempotency # priority: 1500 #3 plugin_attr: #4 idempotency: host: redis #5 स्थैतिक रूट कॉन्फ़िगरेशन के लिए APISIX कॉन्फ़िगर करें हमारे प्लगइन का स्थान कॉन्फ़िगर करें कस्टम प्लगइन्स को स्पष्ट रूप से घोषित किया जाना चाहिए। प्राथमिकता टिप्पणी आवश्यक नहीं है, लेकिन यह अच्छा अभ्यास है और रखरखाव में सुधार करता है सभी रूटों पर सामान्य प्लगइन कॉन्फ़िगरेशन नीचे देखें अंत में, हम अपना एकल मार्ग घोषित करते हैं: routes: - uri: /* plugins: idempotency: ~ #1 upstream: nodes: "httpbin.org:80": 1 #2 #END #3 उस प्लगइन की घोषणा करें जिसे हम बनाने जा रहे हैं। httpbin एक उपयोगी अपस्ट्रीम है क्योंकि हम विभिन्न URI और विधियों को आज़मा सकते हैं। स्थैतिक रूट कॉन्फ़िगरेशन के लिए अनिवार्य! इस बुनियादी ढांचे के तैयार हो जाने पर हम कार्यान्वयन शुरू कर सकते हैं। प्लगइन का लेआउट तैयार करना अपाचे APISIX प्लगइन की नींव बहुत बुनियादी है: local plugin_name = "idempotency" local _M = { version = 1.0, priority = 1500, schema = {}, name = plugin_name, } return _M अगला चरण कॉन्फ़िगरेशन है, Redis होस्ट और पोर्ट। शुरुआत के लिए, हम सभी रूट पर एक ही Redis कॉन्फ़िगरेशन प्रदान करेंगे। फ़ाइल में अनुभाग के पीछे यही विचार है: सामान्य कॉन्फ़िगरेशन। आइए अपने प्लगइन को विस्तार से देखें: उदाहरण के लिए, config.yaml plugin_attr local core = require("apisix.core") local plugin = require("apisix.plugin") local attr_schema = { --1 type = "object", properties = { host = { type = "string", description = "Redis host", default = "localhost", }, port = { type = "integer", description = "Redis port", default = 6379, }, }, } function _M.init() local attr = plugin.plugin_attr(plugin_name) or {} local ok, err = core.schema.check(attr_schema, attr) --2 if not ok then core.log.error("Failed to check the plugin_attr[", plugin_name, "]", ": ", err) return false, err end end कॉन्फ़िगरेशन का आकार परिभाषित करें जाँचें कि कॉन्फ़िगरेशन वैध है क्योंकि मैंने प्लगइन में डिफ़ॉल्ट मान परिभाषित किए हैं, इसलिए मैं अपने डॉकर कंपोज़ इंफ्रास्ट्रक्चर के अंदर चलाने के लिए केवल को पर ओवरराइड कर सकता हूं और डिफ़ॉल्ट पोर्ट का उपयोग कर सकता हूं। host redis इसके बाद, मुझे Redis क्लाइंट बनाना होगा। ध्यान दें कि प्लेटफ़ॉर्म मुझे रीराइट/एक्सेस सेक्शन के बाद किसी भी चरण में कनेक्ट होने से रोकता है। इसलिए, मैं इसे विधि में बनाऊँगा और अंत तक रखूँगा। init() local redis_new = require("resty.redis").new --1 function _M.init() -- ... redis = redis_new() --2 redis:set_timeout(1000) local ok, err = redis:connect(attr.host, attr.port) if not ok then core.log.error("Failed to connect to Redis: ", err) return false, err end end OpenResty Redis मॉड्यूल के फ़ंक्शन का संदर्भ लें। new उदाहरण पाने के लिए इसे कॉल करें. Redis क्लाइंट अब शेष प्लगइन निष्पादन चक्र के दौरान वेरिएबल में उपलब्ध है। redis नाममात्र पथ का क्रियान्वयन अपने पिछले सॉफ़्टवेयर इंजीनियर जीवन में, मैंने आमतौर पर पहले नाममात्र पथ को लागू किया। उसके बाद, मैंने त्रुटि मामलों को अलग-अलग प्रबंधित करके कोड को और अधिक मज़बूत बनाया। इस तरह, अगर मुझे किसी भी बिंदु पर रिलीज़ करना पड़ा, तो मैं अभी भी व्यावसायिक मूल्य प्रदान करूँगा - चेतावनियों के साथ। मैं इस मिनी-प्रोजेक्ट को उसी तरह से देखूँगा। नाममात्र पथ पर छद्म एल्गोरिथ्म निम्नलिखित जैसा दिखता है: DO extract idempotency key from request DO look up value from Redis IF value doesn't exist DO set key in Redis with empty value ELSE RETURN cached response DO forward to upstream DO store response in Redis RETURN response हमें तर्क को उस चरण से मैप करना होगा जिसका मैंने ऊपर उल्लेख किया है। अपस्ट्रीम से पहले दो चरण उपलब्ध हैं, और ; उसके बाद तीन, , और । चरण पहले काम के लिए स्पष्ट लग रहा था, लेकिन मुझे तीन अन्य के बीच का पता लगाना था। मैंने बेतरतीब ढंग से चुना, लेकिन मैं अन्य चरणों के लिए समझदार तर्क सुनने के लिए तैयार हूँ। रीराइट एक्सेस हेडर_फ़िल्टर बॉडी_फ़िल्टर लॉग एक्सेस बॉडी_फ़िल्टर ध्यान दें कि मैंने कोड को अधिक पठनीय बनाने के लिए लॉग हटा दिए हैं। डिबगिंग उत्पादन समस्याओं को आसान बनाने के लिए त्रुटि और सूचनात्मक लॉग आवश्यक हैं। function _M.access(conf, ctx) local idempotency_key = core.request.header(ctx, "Idempotency-Key") --1 local redis_key = "idempotency#" .. idempotency_key --2 local resp, err = redis:hgetall(redis_key) --3 if not resp then return end if next(resp) == nil then --4 local resp, err = redis:hset(redis_key, "request", true ) --4 if not resp then return end else local data = normalize_hgetall_result(resp) --5 local response = core.json.decode(data["response"]) --6 local body = response["body"] --7 local status_code = response["status"] --7 local headers = response["headers"] for k, v in pairs(headers) do --7 core.response.set_header(k, v) end return core.response.exit(status_code, body) --8 end end अनुरोध से idempotency कुंजी निकालें. कुंजी को उपसर्ग करें ताकि हम संभावित टकराव से बच सकें। आइडेम्पोटेंसी कुंजी के अंतर्गत Redis में संग्रहीत डेटा सेट प्राप्त करें। यदि कुंजी नहीं मिलती है, तो उसे बूलियन चिह्न के साथ संग्रहीत करें। कस्टम उपयोगिता फ़ंक्शन के माध्यम से Lua तालिका में डेटा को रूपांतरित करें। हेडर को ध्यान में रखते हुए प्रतिक्रिया JSON प्रारूप में संग्रहीत की जाती है। प्रतिक्रिया का पुनर्निर्माण करें. क्लाइंट को पुनर्निर्मित प्रतिक्रिया लौटाएँ। स्टेटमेंट पर ध्यान दें: APISIX बाद के जीवनचक्र चरणों को छोड़ देता है। return function _M.body_filter(conf, ctx) local idempotency_key = core.request.header(ctx, "Idempotency-Key") --1 local redis_key = "idempotency#" .. idempotency_key if core.response then local response = { --2 status = ngx.status, body = core.response.hold_body_chunk(ctx, true), headers = ngx.resp.get_headers() } local redis_key = "idempotency#" .. redis_key local resp, err = red:set(redis_key, "response", core.json.encode(response)) --3 if not resp then return end end end अनुरोध से idempotency कुंजी निकालें. किसी प्रतिक्रिया के विभिन्न तत्वों को Lua तालिका में व्यवस्थित करें। JSON-एन्कोडेड प्रतिक्रिया को Redis सेट में संग्रहीत करें परीक्षणों से पता चला कि यह अपेक्षा के अनुरूप काम करता है। कोशिश करना: curl -i -X POST -H 'Idempotency-Key: A' localhost:9080/response-headers\?freeform=hello curl -i -H 'Idempotency-Key: B' localhost:9080/status/250 curl -i -H 'Idempotency-Key: C' -H 'foo: bar' localhost:9080/status/250 इसके अलावा, तीसरे अनुरोध के लिए एक बेमेल आइडेम्पोटेंसी कुंजी, पुनः उपयोग करने का प्रयास करें। चूंकि हमने अभी तक कोई त्रुटि प्रबंधन लागू नहीं किया है, इसलिए आपको किसी अन्य अनुरोध के लिए कैश्ड प्रतिक्रिया मिलेगी। अब समय आ गया है कि हम अपने खेल को आगे बढ़ाएँ। जैसे कि A त्रुटि पथों का कार्यान्वयन विनिर्देश कई त्रुटि पथ परिभाषित करता है: Idempotency-कुंजी गायब है. Idempotency-Key पहले से ही उपयोग किया गया है. इस Idempotency-Key के लिए एक अनुरोध लंबित है आइए इन्हें एक-एक करके लागू करें। सबसे पहले, आइए जाँचें कि अनुरोध में एक आइडेम्पोटेंसी कुंजी है। ध्यान दें कि हम प्लगइन को प्रति-रूट के आधार पर कॉन्फ़िगर कर सकते हैं, इसलिए यदि रूट में प्लगइन शामिल है, तो हम यह निष्कर्ष निकाल सकते हैं कि यह अनिवार्य है। function _M.access(conf, ctx) local idempotency_key = core.request.header(ctx, "Idempotency-Key") if not idempotency_key then return core.response.exit(400, "This operation is idempotent and it requires correct usage of Idempotency Key") end -- ... अगर चाबी गुम हो तो उचित 400 लौटा दें। यह आसान था। किसी भिन्न अनुरोध के लिए किसी मौजूदा कुंजी के पुनः उपयोग की जाँच करना थोड़ा अधिक जटिल है। हमें सबसे पहले अनुरोध को संग्रहीत करने की आवश्यकता है, या अधिक सटीक रूप से, अनुरोध का गठन करने वाले फिंगरप्रिंट को संग्रहीत करने की आवश्यकता है। दो अनुरोध समान हैं यदि उनमें: समान विधि, समान पथ, समान बॉडी और समान हेडर हैं। आपकी स्थिति के आधार पर, डोमेन (और पोर्ट) उनका हिस्सा हो सकता है या नहीं भी हो सकता है। मेरे सरल कार्यान्वयन के लिए, मैं इसे छोड़ दूंगा। हल करने के लिए कई समस्याएँ हैं। सबसे पहले, मुझे ऑब्जेक्ट को हैश करने के लिए कोई मौजूदा API नहीं मिला, जैसा कि अन्य भाषाओं में है, जिनसे मैं अधिक परिचित हूँ, , Java का । मैंने JSON में ऑब्जेक्ट को एनकोड करने और स्ट्रिंग को हैश करने का निर्णय लिया। हालाँकि, मौजूदा में ऐसे उप-तत्व हैं जिन्हें JSON में परिवर्तित नहीं किया जा सकता है। मुझे ऊपर बताए गए भागों को निकालना था और तालिका को परिवर्तित करना था। core.request जैसे कि Object.hash() core.request local function hash_request(request, ctx) local request = { --1 method = core.request.get_method(), uri = ctx.var.request_uri, headers = core.request.headers(), body = core.request.get_body() } local json = core.json.stably_encode(request) --2 return ngx.encode_base64(json) --3 end केवल प्रासंगिक भागों वाली एक तालिका बनाएं। लाइब्रेरी JSON बनाती है जिसके सदस्य कई कॉल में अलग-अलग तरीके से सॉर्ट किए जा सकते हैं। इसलिए, यह अलग-अलग हैश में परिणामित होता है। उस समस्या को ठीक करता है। cjson core.json.stably_encode इसे हैश करें। फिर, अनुरोध प्राप्त करते समय बूलियन संग्रहीत करने के बजाय, हम परिणामी हैश संग्रहीत करते हैं। local hash = hash_request(core.request, ctx) if next(resp) == nil then core.log.warn("No key found in Redis for Idempotency-Key, set it: ", redis_key) local resp, err = redis:hset(redis_key, "request", hash) if not resp then core.log.error("Failed to set data in Redis: ", err) return end then -- ... हम दूसरी शाखा पर idempotency कुंजी के अंतर्गत संग्रहीत हैश को पढ़ते हैं। यदि वे मेल नहीं खाते हैं, तो हम संबंधित त्रुटि कोड के साथ बाहर निकलते हैं: local data = normalize_hgetall_result(resp) local stored_hash = data["request"] if hash ~= stored_hash then return core.response.exit(422, "This operation is idempotent and it requires correct usage of Idempotency Key. Idempotency Key MUST not be reused across different payloads of this operation.") end अंतिम त्रुटि प्रबंधन इसके ठीक बाद होता है। निम्नलिखित परिदृश्य की कल्पना करें: एक अनुरोध idempotency कुंजी X के साथ आता है. प्लगइन फिंगरप्रिंट्स लेता है और हैश को रेडिस में संग्रहीत करता है। APISIX अनुरोध को अपस्ट्रीम पर अग्रेषित करता है। डुप्लिकेट अनुरोध समान आइडेम्पोटेंसी कुंजी, X के साथ आता है। प्लगइन Redis से डेटा पढ़ता है और कोई कैश्ड प्रतिक्रिया नहीं पाता है। अपस्ट्रीम ने अनुरोध का प्रसंस्करण पूरा नहीं किया; इसलिए, पहला अनुरोध अभी तक चरण तक नहीं पहुंचा है। body_filter हम उपरोक्त स्निपेट में निम्नलिखित कोड जोड़ते हैं: if not data["response"] then return core.response.exit(409, " request with the same Idempotency-Key for the same operation is being processed or is outstanding.") end इतना ही। निष्कर्ष इस पोस्ट में, मैंने प्लगइन के माध्यम से अपाचे APISIX पर हेडर विनिर्देश का एक सरल कार्यान्वयन दिखाया। इस स्तर पर, इसमें सुधार की गुंजाइश है: स्वचालित परीक्षण, प्रति रूट के आधार पर Redis को कॉन्फ़िगर करने की क्षमता, अनुरोध का हिस्सा बनने के लिए डोमेन/पथ को कॉन्फ़िगर करना, एकल इंस्टेंस के बजाय Redis क्लस्टर को कॉन्फ़िगर करना, किसी अन्य K/V स्टोर का उपयोग करना, आदि। Idempotency-Key फिर भी, यह विनिर्देश को कार्यान्वित करता है और इसमें अधिक उत्पादन-स्तर के कार्यान्वयन के रूप में विकसित होने की क्षमता है। इस पोस्ट का पूरा स्रोत कोड पर पाया जा सकता है। GitHub आगे जाने के लिए: आइडेम्पोटेंसी-कुंजी HTTP हेडर फ़ील्ड डुप्लिकेट API अनुरोधों को ठीक करना प्लगइन डेवलप - APISIX वेबसाइट 0 से 1 तक Apache APISIX प्लगइन कैसे बनाएं? मूल रूप से 7 अप्रैल, 2024 को पर प्रकाशित A Java Geek