Locust में अपने statistics बिगाड़े बिना कैप्चा कैसे हल करें

locust captcha - How to Solve CAPTCHAs in Locust Without Skewing Your Stats

Locust में कैप्चा हल करने का काम दो जगहों से बाहर रहना चाहिए: आपके statistics से, और per-iteration रास्ते से। Locust हर उस request की रिपोर्ट करता है जो self.client के ज़रिए जाती है, इसलिए उसी के ज़रिए हल करने पर सॉल्वर के response times उसी रिपोर्ट में आ गिरते हैं जिसे आप पढ़ने की कोशिश कर रहे हैं। और किसी task के अंदर रखा गया solve हर यूज़र के हर iteration पर चलता है। एक बार सीमाएँ पता चल जाएँ, तो दोनों से बचना मुश्किल नहीं है।

आपको क्या चाहिए

  • Python 3.10 या उससे नए पर Locust 2.x, जो CapSkip client के लिए भी ज़रूरी है।
  • किसी Windows मशीन पर चलता हुआ CapSkip, और आपके locustfile के साथ इंस्टॉल किया हुआ Python client।
  • सुरक्षित फ़ॉर्म का sitekey और page URL, जो हर यूज़र दोबारा खोजे नहीं बल्कि बाहर से पास किया जाए।
  • Server mode, अगर Locust सॉल्वर की अपनी मशीन के अलावा कहीं भी चलता है, जिसमें हर कंटेनर और हर worker बॉक्स शामिल है। यह कनेक्शन सेटिंग्स के अंदर बस एक सेटिंग है।
# pip install capskip
pip install -U locust capskip

चरण 1: solve को self.client से दूर रखें

यही वह हिस्सा है जो चुपचाप रिपोर्ट बर्बाद कर देता है। Locust के डॉक्यूमेंटेशन में साफ़ लिखा है कि self.client क्या है: यह HttpSession का एक instance है, जो requests.Session का subclass और wrapper है, और यह उसमें जो जोड़ता है वह है request के नतीजों की रिपोर्टिंग Locust में। सफलता और विफलता, response time, response length, नाम। इसके ज़रिए भेजी गई हर चीज़ statistics टेबल में पहुँच जाती है।

एक solve में सेकंड लगते हैं। आपके एप्लिकेशन के endpoints मिलीसेकंड लेते हैं। दोनों को एक ही टेबल में डाल दीजिए और आपका पंचानवेवाँ percentile इस बात का माप बन जाता है कि कैप्चा में कितना समय लगा, और यह वह संख्या नहीं है जो किसी ने माँगी थी।

अच्छी ख़बर यह है कि इसका फिक्स है: कुछ मत कीजिए। CapSkip client के पास अपना HTTP transport है और वह self.client को कभी छूता ही नहीं, इसलिए डिफ़ॉल्ट रूप से solve Locust के statistics को दिखता ही नहीं। Locust सिर्फ़ उसी को लॉग करता है जो उसके अपने session से गुज़रता है।

# pip install capskip
import os
from capskip import CapSkip

# CAPSKIP_HOST is the solver machine. 127.0.0.1 only works when
# Locust and CapSkip run on the same Windows box.
solver = CapSkip(
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", "8080")),
    apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
)

def fresh_token(sitekey, page_url):
    result = solver.recaptcha(sitekey=sitekey, url=page_url)
    return result["code"]   # the token, and Locust never sees it

अगर आप raw endpoints पर ख़ुद हाथ से कोड लिखना चाहें, तो नियम वही रहता है: self.client नहीं, एक सादा requests.Session इस्तेमाल कीजिए। सीधे requests लाइब्रेरी से की गई requests को Locust लॉग नहीं करता, और यहाँ आपको ठीक यही चाहिए। वे दोनों endpoints यहाँ डॉक्यूमेंट किए गए हैं: CapSkip API डॉक्यूमेंटेशन.

इसका उलटा करने का एक ही मौक़ा है: जब आप जानबूझकर सॉल्वर का ही load test कर रहे हों। तब उसे name argument के साथ self.client से भेजिए, ताकि हर solve हर sitekey के लिए अलग पंक्ति बनने के बजाय एक ही पंक्ति में समूहबद्ध हो जाए, और उस पंक्ति को बाक़ी सबसे अलग पढ़िए।

चरण 2: आपका solve असल में कितनी बार चलता है?

Locust इसे रखने के लिए चार जगहें देता है और उनमें कई गुना का फ़र्क़ है। कोई एक चुनने से पहले गुणक निकाल लीजिए।

solve कहाँ बैठता हैयह कितनी बार चलता है
किसी task फ़ंक्शन के अंदरहर यूज़र के हर iteration पर एक बार। प्रति मिनट हज़ारों
on_start मेथड मेंहर सिमुलेटेड यूज़र पर एक बार। पाँच सौ यूज़र यानी पाँच सौ solves
किसी test_start लिसनर मेंहर node पर एक बार, जो हर run पर एक बार नहीं है। चरण 3 देखें
master तक सीमित किए गए test_start लिसनर मेंहर run पर एक बार, और फिर उसे बाँटना पड़ता है

लगभग सबके लिए डिफ़ॉल्ट जवाब on_start है। कोई यूज़र चलना शुरू करते ही on_start को कॉल करता है, इसलिए हर सिमुलेटेड यूज़र को अपना token मिलता है, वह उसे अपने session भर के लिए रखता है, और greenlets के बीच कुछ भी पास करने की ज़रूरत नहीं पड़ती। यही आकार उस चीज़ से भी मेल खाता है जो कोई असली यूज़र करता है।

from locust import HttpUser, task, between

class Signup(HttpUser):
    wait_time = between(1, 3)

    def on_start(self):
        # One solve per simulated user, off the statistics.
        self.token = fresh_token(SITEKEY, PAGE_URL)

    @task
    def submit(self):
        self.client.post("/signup", data={
            "email": "[email protected]",
            "g-recaptcha-response": self.token,
        })

पाँच सौ solves महँगे लगते हैं, क्योंकि मीटर वाली सेवा के साथ वे महँगे होते भी हैं। यही वजह है कि ज़्यादातर load testing गाइड एक बार हल करके नतीजा बाँटने के जुगाड़ में उलझ जाते हैं। अपने ही hardware पर चल रहे सॉल्वर के साथ यह संख्या बजट का सवाल नहीं रह जाती, वह क्षमता का सवाल बन जाती है, और उसका जवाब देना कहीं आसान है: ramp चलाइए और मशीन पर नज़र रखिए।

चरण 3: test_start हर node पर चलता है, हर run पर एक बार नहीं

यह Locust की वह बारीकी है जो लोगों को चौंकाती है, और यह solve की ऐसी गिनती के रूप में सामने आती है जो किसी संख्या का साफ़ गुणज होती है। डॉक्यूमेंटेशन कहता है कि जब नया load test शुरू होता है तो test_start हर node पर फ़ायर होता है। चार worker प्रोसेस के साथ चलाइए और आपके पास पाँच nodes होते हैं, यानी सादे test_start लिसनर में रखा गया solve पाँच बार चलता है।

runner का प्रकार जाँचकर इसे रोकिए। Locust ठीक इसी के लिए MasterRunner और WorkerRunner देता है, और उसकी अपनी distributed गाइड का पैटर्न यही है कि आप जाँचें कि आप किस पर हैं।

from locust import events
from locust.runners import WorkerRunner

@events.init.add_listener
def on_init(environment, **kwargs):
    # Workers listen. Registering here runs before the test starts.
    if isinstance(environment.runner, WorkerRunner):
        environment.runner.register_message("captcha_token", take_token)

@events.test_start.add_listener
def on_test_start(environment, **kwargs):
    # The master solves once and broadcasts. Workers skip this.
    if not isinstance(environment.runner, WorkerRunner):
        environment.runner.send_message(
            "captcha_token", fresh_token(SITEKEY, PAGE_URL)
        )

def take_token(environment, msg, **kwargs):
    environment.shared_token = msg.data

उस ब्लॉक के बारे में दो बातें। handler का signature तय है: यह environment, message और keyword arguments लेता है, और payload msg.data के रूप में आता है। और अगर किसी handler को समय लगने वाला है, तो उसे concurrent को True सेट करके register कीजिए, ताकि वह Locust की heartbeat और दूसरे system messages को ब्लॉक करने के बजाय अपने ही greenlet में चले। जो handler सिर्फ़ एक string स्टोर करता है उसे इसकी ज़रूरत नहीं। जो handler अपने अंदर हल करता है, उसे ज़रूरत है।

इसमें से कुछ भी बनाने से पहले अगला सेक्शन पढ़ लीजिए, क्योंकि हर run पर एक solve वैसे भी आमतौर पर ग़लत लक्ष्य है।

चरण 4: लंबे टेस्ट में token टिकता नहीं

reCAPTCHA token लगभग दो मिनट तक चलता है। load test आमतौर पर दो मिनट से लंबा होता है। इसलिए वह सुथरा आर्किटेक्चर, यानी टेस्ट की शुरुआत में एक solve और उसे हर worker तक broadcast, ऐसा run पैदा करता है जिसमें पहले कुछ मिनट पास हो जाते हैं और उसके बाद सब कुछ expire हो चुके token पर फ़ेल होता है, और वे विफलताएँ आपके एप्लिकेशन के खाते में दर्ज होती हैं।

उस विफलता को देखते ही पहचान लेना फ़ायदेमंद है, और यही वह विफलता है जो queue jobs को एक के बाद एक जोड़ने वालों को भी पकड़ती है। इसे इस गाइड में विस्तार से समझाया गया है: reCAPTCHA token की समय समाप्ति.

इसलिए शेयर्ड token वाला पैटर्न सिर्फ़ तभी इस्तेमाल कीजिए जब टेस्ट छोटा हो, या जब token हर iteration के बजाय ramp-up के दौरान एक ही बार चाहिए हो। बाक़ी हालात में on_start में हर यूज़र के लिए हल कीजिए, और घंटों चलने वाले soak test के लिए यूज़र के अंदर ही एक तय समय पर token रिफ़्रेश कीजिए।

import time

class Signup(HttpUser):
    wait_time = between(1, 3)

    def on_start(self):
        self.token = fresh_token(SITEKEY, PAGE_URL)
        self.solved_at = time.monotonic()

    @task
    def submit(self):
        # Refresh before the token ages out, not after it fails.
        if time.monotonic() - self.solved_at > 90:
            self.token = fresh_token(SITEKEY, PAGE_URL)
            self.solved_at = time.monotonic()
        self.client.post("/signup", data={
            "g-recaptcha-response": self.token,
        })

एक सौ बीस के बजाय नब्बे सेकंड, ताकि रिफ़्रेश तब हो जब token अभी वैध हो।

चरण 5: Locust gevent पर चलता है, इसलिए सिंक्रोनस client इस्तेमाल करें

Locust हर यूज़र को उसके अपने greenlet के अंदर चलाता है और gevent का इस्तेमाल करते हुए event आधारित है। उसका डॉक्यूमेंटेशन यह बात रेखांकित करता है कि इसी वजह से आप अपने टेस्ट callbacks के बजाय सामान्य blocking Python कोड की तरह लिख पाते हैं। यहाँ blocking ही स्वाभाविक शैली है, इसलिए सादा CapSkip client ही उठाने लायक़ है।

locustfile में AsyncCapSkip की तरफ़ मत जाइए। Python में यह किसी alias के बजाय सचमुच का async implementation है, जिससे यह किसी asyncio प्रोग्राम में सही client बनता है, और Locust asyncio प्रोग्राम नहीं है। उसके लिए कोई event loop इंतज़ार नहीं कर रहा, और greenlet के अंदर हर यूज़र के लिए एक event loop शुरू करना उतनी ही चीज़ वापस पाने के लिए बहुत सारा ताम झाम है जो gevent पहले से कर देता है।

polling का व्यवहार भी यहाँ मदद करता है। client एक समान अंतराल पर poll नहीं करता। वह एक चौथाई सेकंड से शुरू होता है और pollingInterval की ओर बढ़ते हुए धीमा होता जाता है, इसलिए तेज़ solve किसी तय देरी का इंतज़ार करने के बजाय तेज़ी से लौट आता है। एक साथ ramp कर रहे पाँच सौ greenlets में यही फ़र्क़ आपके ramp का ज़्यादातर हिस्सा है। अगर कहीं और किसी asyncio प्रोग्राम में आपको सचमुच कैप्चा का पूरा बैच एक साथ हल करना है, तो वह स्थिति यहाँ कवर की गई है: Python में समानांतर रूप से कैप्चा हल करना.

चरण 6: सॉल्वर को वहाँ चलाना जहाँ workers उस तक पहुँच सकें

Load generators शायद ही कभी किसी और चीज़ वाली ही मशीन पर बैठते हैं। उन्हें अपना बॉक्स मिलता है, या कई बॉक्स, या कंटेनरों का एक पूल, ठीक इसीलिए कि load असली रहे। CapSkip Windows पर चलता है, और ज़रूरी नहीं कि आपके workers भी वहीं चलें।

दो कनेक्शन मोड होते हैं। Local 127.0.0.1 पर bind होता है और सिर्फ़ उसी डिवाइस को जवाब देता है। Server आपके नेटवर्क address या पब्लिक IP पर bind होता है, ताकि कोई दूसरा बॉक्स, कोई कंटेनर होस्ट या कोई hosted runner उसी Windows मशीन तक API के ज़रिए पहुँच सके। दोनों एक ही जगह मिलते हैं: कनेक्शन सेटिंग्स, और Server mode सिर्फ़ यह बदलता है कि सॉल्वर किस address पर सुनता है। hardware अब भी आपका ही है और हल अब भी बिना मीटर वाला है।

Locust कहाँ चलता हैकौन-सा कनेक्शन मोड
CapSkip वाली ही Windows मशीन, single processLocal mode, होस्ट 127.0.0.1 ही रहता है
आपके नेटवर्क की दूसरी मशीनों पर worker प्रोसेससॉल्वर के LAN पते के साथ Server mode
कंटेनर या कोई hosted runnerस्टैटिक पब्लिक IP और एक फ़ायरवॉल नियम के साथ Server mode

address को locustfile से नहीं, environment से पढ़िए। Python client CAPSKIP_HOST, CAPSKIP_PORT या CAPSKIP_API_KEY को अपने आप नहीं पढ़ता, इसीलिए ऊपर वाला solver इन्हें पढ़कर client को पास करता है, और इसी वजह से वही फ़ाइल आपके लैपटॉप पर और किसी worker fleet पर बिना बदलाव के चलती है।

आम errors और उनका मतलब

आप जो देखते हैंकारणफिक्स
Locust statistics टेबल में सॉल्वर की पंक्तियाँsolve self.client से होकर गयाCapSkip client इस्तेमाल करें, या सादा requests.Session
ऐप असल में जो करता है उससे कहीं ऊपर के percentilesवही कारण। solve के समय औसत में मिला दिए जा रहे हैंवही फिक्स। और कुछ बदलने की ज़रूरत नहीं
solve की गिनती आपके worker की गिनती का गुणज हैtest_start हर node पर फ़ायर होता हैलिसनर को WorkerRunner जाँच से रोकें
run के दो एक मिनट बाद सब कुछ फ़ेल हो जाता हैटेस्ट की शुरुआत में एक ही token हल हुआ था और वह expire हो चुका हैहर यूज़र के लिए हल करें, या task के अंदर रिफ़्रेश करें
एक साथ हर यूज़र से NetworkExceptionCapSkip loopback पर है और Locust कहीं औरServer mode पर जाएँ और CAPSKIP_HOST सेट करें
distributed run के दौरान heartbeat चेतावनियाँकोई धीमा message handler runner को ब्लॉक कर रहा हैउसे concurrent को True सेट करके register करें
किसी ApiException के अंदर ERROR_WRONG_USER_KEYवर्कर एनवायरनमेंट में CAPSKIP_API_KEY सेट नहीं हैउसे workers पर सेट करें और उन्हें रीस्टार्ट करें

आख़िरी वाली का अपना अलग गाइड है, क्योंकि वही response उस key को भी कवर करता है जो मौजूद ही नहीं है और उस key को भी जो बस ग़लत है: ERROR_WRONG_USER_KEY कैसे ठीक करें.

FAQ

क्या मुझे हर टेस्ट पर एक बार हल करना चाहिए या हर यूज़र पर?

हर यूज़र पर एक बार, बशर्ते पूरा टेस्ट दो मिनट से कम का न हो। किसी असली load test के ख़त्म होने से बहुत पहले token expire हो जाता है, इसलिए शेयर्ड token वाला संस्करण चुपचाप आपके error path का टेस्ट बन जाता है। हर यूज़र के लिए हल करना ज़्यादा ईमानदार सिमुलेशन भी है, क्योंकि असली यूज़र भी अपना अपना token लाते हैं। इससे बचने की इकलौती वजह प्रति हल बिलिंग है, और आपके अपने hardware पर चलने वाला सॉल्वर वह वजह हटा देता है।

क्या solve मेरे requests per second में गिना जाता है?

नहीं, जब तक वह self.client से होकर न जाए। Locust अपने statistics अपने ही session से बनाता है, इसलिए CapSkip client या सादे requests.Session से भेजी गई कोई भी चीज़ रिपोर्ट को दिखती नहीं। यही डिफ़ॉल्ट व्यवहार है और इसके लिए कोई कॉन्फ़िगरेशन नहीं चाहिए।

क्या यह FastHttpUser के साथ काम करता है?

हाँ, और कुछ भी नहीं बदलता। FastHttpUser client को एक तेज़ implementation से बदल देता है, जो तब करने लायक़ है जब load generator ख़ुद अड़चन बन रहा हो, लेकिन solve तो कभी उस client से गुज़रा ही नहीं था। on_start मेथड और event listeners बिल्कुल वैसे ही व्यवहार करते हैं।

यह k6 गाइड से कैसे अलग है?

सलाह लगभग उलटी चलती है, और इसकी ठोस वजह है। k6 कोई Node पैकेज इंस्टॉल नहीं कर सकता, इसलिए वह गाइड raw HTTP API से काम चलाती है और setup स्टेज में एक ही बार हल करती है, क्योंकि वहाँ उसे दोहराना सचमुच असुविधाजनक है। Locust Python है, client सामान्य तरीक़े से इंस्टॉल हो जाता है, और हर यूज़र के लिए हल करना आसान भी है और ज़्यादा सटीक भी। तुलना यहाँ दी गई है: k6 load test गाइड.

संक्षेप में

CapSkip client से हल कीजिए, ताकि request न कभी self.client तक पहुँचे और न कभी आपके statistics तक। कॉल को on_start में रखिए ताकि हर सिमुलेटेड यूज़र को एक token मिले, और अगर run लगभग नब्बे सेकंड से लंबा चले तो उसे task के अंदर रिफ़्रेश कीजिए। अगर आप test_start लिसनर में ही हल करते हैं, तो उसे WorkerRunner जाँच से रोकिए, क्योंकि वह event हर node पर फ़ायर होता है। सिंक्रोनस client पर ही रहिए, क्योंकि Locust की concurrency gevent है। जब भी load generators सॉल्वर की अपनी मशीन पर न हों, CapSkip को Server mode में चलाइए।

ramp का आकार तय करने से पहले एक बात साफ़ कर लीजिए: CapSkip एक असीमित कैप्चा सॉल्वर है, जो उस hardware पर चलता है जो पहले से आपका है, इसलिए एक हज़ार सिमुलेटेड यूज़र में से हर एक अपना चैलेंज हल करे तब भी ख़र्च ठीक उतना ही होता है जितना उनमें से दस का।