BullMQ Worker (Node.js) में कैप्चा कैसे हल करें

BullMQ का कैप्चा worker पहली बार चलाने पर ठीक ही चलता है, फिर दो चुपचाप तरीक़ों से निराश करता है। जो worker option तय करता है कि एक साथ कितनी jobs चलें, उसका डिफ़ॉल्ट 1 है, इसलिए हल का बैकलॉग एक-एक करके ही निपटता है। और अगर processor ने कभी event loop को बाँध लिया, तो BullMQ मान लेता है कि job अटक गई है, उसे किसी दूसरे worker को सौंप देता है, और वही कैप्चा दो बार हल हो जाता है। इनमें से कोई bug नहीं है। दोनों डिफ़ॉल्ट हैं, और दोनों एक लाइन में बदले जा सकते हैं।
आपको क्या चाहिए
- Redis, साथ ही आपके workers चलाने वाले प्रोजेक्ट में इंस्टॉल किया हुआ BullMQ।
- किसी Windows मशीन पर चलता हुआ CapSkip, और उसी प्रोजेक्ट में Node client।
- sitekey और पेज URL hardcode करने के बजाय job data के साथ आएँ, ताकि एक ही queue हर फ़ॉर्म के काम आए।
- scale out करने से पहले तय किया गया कनेक्शन मोड, क्योंकि workers आम तौर पर सॉल्वर से ज़्यादा मशीनों पर जा पहुँचते हैं।
# npm install capskip npm install bullmq capskip
चरण 1: worker कहाँ चलता है, और उसके लिए कौन-सा कनेक्शन मोड चाहिए
यह सबसे पहले तय कर लेना चाहिए, क्योंकि BullMQ की अपनी सलाह यही है कि workers का पूरा बेड़ा कई अलग-अलग मशीनों पर चलाया जाए, और जैसे ही आप ऐसा करते हैं, loopback का वह मतलब नहीं रह जाता जो आपके लैपटॉप पर था।
कनेक्शन के दो मोड हैं। Local mode 127.0.0.1 से बँधता है और सिर्फ़ उसी डिवाइस को जवाब देता है, जो तब सही है जब आपका ऑटोमेशन और सॉल्वर एक ही मशीन पर हों। Server mode आपके नेटवर्क पते या पब्लिक IP से बँधता है, ताकि कोई दूसरी मशीन, कोई VPS या कोई होस्टेड प्लेटफ़ॉर्म उसी Windows मशीन तक API के ज़रिए पहुँच सके। Server mode सिर्फ़ यह बदलता है कि सॉल्वर किस पते पर सुनता है। हार्डवेयर अब भी आपका ही है और इस पर अब भी प्रति-हल कोई शुल्क नहीं लगता। ये दोनों मोड यहाँ मिलते हैं: कनेक्शन सेटिंग्स.
| worker process कहाँ चलता है | कौन-सा कनेक्शन मोड |
|---|---|
| CapSkip मशीन पर, एक ही process | Local mode। 127.0.0.1 सचमुच सही है |
| आपके अपने नेटवर्क पर workers का बेड़ा | सॉल्वर के LAN पते के साथ Server mode |
| containers में, या किसी VPS पर workers | पहुँच योग्य पते और एक firewall नियम के साथ Server mode |
VPS पर चलने वाले किसी worker के लिए एक static पब्लिक IP का इंतज़ाम कर लेना ठीक रहता है, ताकि चलते हुए deployment के नीचे से पता खिसक न जाए। पते को सोर्स कोड में लिखने के बजाय किसी environment variable में रखें, क्योंकि आपके लैपटॉप और आपके workers को अलग-अलग मान चाहिए। client अपने आप कोई भी environment variable नहीं पढ़ता, इसलिए CAPSKIP_HOST को अपने कोड में पढ़ें और client को पास करें, जैसा नीचे वाला worker करता है।
// npm install capskip
import { CapSkip } from "capskip";
// One client, shared by every job this worker handles.
// CAPSKIP_HOST is 127.0.0.1 locally and the solver's
// address on every other machine.
export const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: 8080,
});चरण 2: concurrency का डिफ़ॉल्ट एक बार में एक job है
जब तक आप कुछ और न कहें, BullMQ worker एक बार में एक ही job process करता है। यह डिफ़ॉल्ट CPU वाले काम के लिए समझदारी भरा है और कैप्चा हल करने के लिए बहुत ग़लत, क्योंकि हल करना लगभग पूरा का पूरा इंतज़ार ही है। BullMQ यह सीधे कहता है: concurrency तभी संभव है जब workers asynchronous काम करें, जैसे किसी डेटाबेस या किसी बाहरी HTTP सेवा को कॉल। HTTP पर सॉल्वर को poll करना ठीक इसी शक्ल का काम है, इसलिए event loop पूरे समय ख़ाली रहता है।
यह हिसाब एक बार लगा लेना चाहिए। अगर एक reCAPTCHA हल होने में बीस सेकंड लगते हैं, तो डिफ़ॉल्ट पर एक worker मिनट में तीन jobs निपटाता है। वही worker बीस की concurrency पर, उसी हार्डवेयर पर, साठ निपटाता है, क्योंकि उनमें से उन्नीस jobs CPU के लिए होड़ करने के बजाय किसी socket read में बैठी रहती हैं।
import { Worker } from "bullmq";
import { solver } from "./solver.js";
// A solve is an awaited HTTP call, so raising this costs
// almost no CPU. Size it for the solver machine and for
// what the target site will accept.
const worker = new Worker("captcha", async (job) => {
const { sitekey, pageUrl } = job.data;
const result = await solver.recaptcha(sitekey, pageUrl);
return await submitForm(pageUrl, result.code);
}, { connection: { host: "127.0.0.1", port: 6379 }, concurrency: 20 });प्रति-हल शुल्क वाले सॉल्वर के साथ यह संख्या असल में ख़र्च पर लगाम होती है, इसीलिए इतने सारे उदाहरण इसे डरा-सहमा सा रखते हैं। यहाँ यह एक मशीन की क्षमता का सवाल है, और इस बात का कि आप जिस साइट पर सबमिट कर रहे हैं वह कितनी तेज़ी से requests स्वीकार करेगी। दूसरी बात आम तौर पर ज़्यादा कसी हुई सीमा होती है।
चरण 3: job stall क्यों होती है, और stall होने का मतलब दो बार हल करना क्यों है
यही वह गड़बड़ी है जो लोगों की पूरी सुबह खा जाती है, क्योंकि logs देखकर लगता है कि queue ठीक चल रही है जबकि सॉल्वर का अपना log कुछ और कहता है।
जब कोई job किसी worker तक पहुँचती है, तो BullMQ उस पर एक lock लगा देता है ताकि और कोई उसे छू न सके, और worker को queue से लगातार कहते रहना पड़ता है कि वह अब भी काम कर रहा है। यह lock lockDuration है, डिफ़ॉल्ट रूप से 30000 ms, और worker उसे उसके आधे अंतराल पर renew करता रहता है। एक अलग सफ़ाई, stalledInterval, हर 30000 ms पर चलती है जो ऐसे locks ढूँढ़ती है जिन्हें किसी ने renew नहीं किया। अगर worker इतना व्यस्त था कि समय पर renew न कर सका, तो job stalled मान ली जाती है, वापस waiting में डाल दी जाती है, और कोई दूसरा worker उसे फिर से process करता है। जब वह maxStalledCount की इजाज़त से ज़्यादा बार stall हो जाए, और उसका डिफ़ॉल्ट एक है, तो वह इसके बजाय failed सेट में चली जाती है।
तो stalled job कोई failed job नहीं है, और उसका दोबारा चलना कोई retry नहीं है। यह queue का सही अनुमान है कि उस job को पकड़े हुए worker मर चुका है। आपका सॉल्वर एक ही कैप्चा के लिए दो submissions देखता है, और इस्तेमाल सिर्फ़ दूसरा token ही होता है।
| Worker सेटिंग | डिफ़ॉल्ट | यह क्या तय करती है |
|---|---|---|
| "concurrency" | 1 | एक worker एक साथ कितनी jobs सँभालता है |
| "lockDuration" | 30000 ms | renew हुए बिना lock कितनी देर टिकता है |
| "lockRenewTime" | lockDuration का आधा | worker इसे कितनी-कितनी देर में renew करता है |
| "stalledInterval" | 30000 ms | बिना renew हुए locks कितनी-कितनी देर में समेटे जाते हैं |
| "maxStalledCount" | 1 | job के fail होने से पहले वह कितनी बार दोबारा चलेगी |
वजह हमेशा एक ही होती है: processor ने CPU पकड़े रखा। await किया हुआ HTTP कॉल ऐसा नहीं करता, इसलिए client की अपनी polling सुरक्षित है। result endpoint पर busy-wait करने वाला हाथ से लिखा loop सुरक्षित नहीं है, और सबमिट करने से पहले किसी बड़ी इमेज को synchronously डिकोड करना भी नहीं। client को ही poll करने दें, क्योंकि वह 250 ms से शुरू होकर धीमा होता जाता है, बजाय किसी एक तय अंतराल पर सोने के, और जो भी क़दम सचमुच CPU-bound हो उसे processor से बाहर ले जाएँ, या किसी sandboxed processor में डाल दें।
lock की अवधि बढ़ा देना पहले उपाय के तौर पर ग़लत है। यह सिर्फ़ लक्षण का इलाज करता है, और जो worker सचमुच मर चुका है उस पर लंबा lock पूरी उस अवधि तक job को अछूता छोड़ देता है।
चरण 4: retries, और वह token जिसे रखना नहीं चाहिए
BullMQ डिफ़ॉल्ट रूप से retry नहीं करता। attempts option 1 है, यानी एक कोशिश और फिर failed सेट। कैप्चा हल करने के लिए यह आम तौर पर ठीक ही है, क्योंकि यहाँ ज़्यादातर विफलताएँ या तो parameter की कोई ग़लती होती हैं जो हमेशा एक जैसी ही विफल होगी, या कोई ऐसी साइट जो आगे बढ़ चुकी है। retries की असली जगह वह worker है जो थोड़ी देर के लिए सॉल्वर तक नहीं पहुँच पाया, तो उन्हें एक backoff दें और गिनती कम रखें।
await queue.add("signup", { sitekey, pageUrl }, {
// Two tries, spaced out, for a solver that was
// briefly unreachable. Not for a bad sitekey.
attempts: 2,
backoff: { type: "exponential", delay: 5000 },
// Do not leave finished jobs sitting in Redis forever.
removeOnComplete: { age: 3600, count: 1000 },
});इसके साथ दो नियम चलते हैं। किसी token को एक attempt से दूसरी attempt तक कभी न ले जाएँ: reCAPTCHA token लगभग दो मिनट तक ही काम का रहता है, इसलिए हल उसी attempt के अंदर करें जो उसे सबमिट करता है, और अगर किसी token को cache करने का मन हो तो token एक्सपायरी गाइड पढ़ें। और token को job के return value के रूप में न लौटाएँ। BullMQ डिफ़ॉल्ट रूप से पूरी हो चुकी jobs रखता है, इसलिए वह return value Redis में जाकर वहीं पड़ा रह जाता है। इसके बजाय submission का नतीजा लौटाएँ, क्योंकि बाद में आप असल में यही देखना चाहते हैं।
पूरा चलने वाला उदाहरण
एक producer, एक worker, और हल उसी processor में जो उसे इस्तेमाल भी करता है।
// npm install capskip
import { Queue, Worker } from "bullmq";
import { CapSkip, ValidationException } from "capskip";
const connection = { host: "127.0.0.1", port: 6379 };
const queue = new Queue("captcha", { connection });
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: 8080,
});
new Worker("captcha", async (job) => {
const { sitekey, pageUrl, email } = job.data;
try {
// Solve and submit together. The token is short lived.
const result = await solver.recaptcha(sitekey, pageUrl);
const res = await postSignup(pageUrl, email, result.code);
return { status: res.status };
} catch (err) {
// A bad sitekey fails the same way on every attempt.
if (err instanceof ValidationException) {
await job.discard();
}
throw err;
}
}, { connection, concurrency: 20 });वह कॉल reCAPTCHA v2 है। बाक़ी प्रकार भी उसी आकार के हैं: invisible या enterprise को 1 सेट करके भेजिए, या version को किसी action के साथ v3 सेट कीजिए, या उसकी जगह turnstile या geetest कॉल कीजिए। पूरा surface यहाँ है: Node.js कैप्चा सॉल्वर पेज.
आम errors और उनका मतलब
| आप जो देखते हैं | कारण | फिक्स |
|---|---|---|
| queue सॉल्वर की रफ़्तार से कहीं धीमे निपटती है | worker concurrency अब भी अपने डिफ़ॉल्ट एक पर है | इसे बढ़ाएँ। हल await किया हुआ I/O है, CPU नहीं |
| एक ही job के लिए दो हल लॉग हुए, कुछ सेकंड के अंतर पर | lock renew नहीं हुआ, इसलिए job stalled गिनी गई | processor में event loop को ब्लॉक करना बंद करें |
| बिना कोई error फेंके job failed सेट में पहुँच जाती है | वह अधिकतम अनुमति से ज़्यादा बार stall हुई | वही ब्लॉक करने वाला काम। उसे ठीक करें, counter को नहीं |
| आपके लैपटॉप पर चलता है, worker मशीन पर NetworkException | उस मशीन पर 127.0.0.1 वही मशीन है | Server mode, और worker के लिए CAPSKIP_HOST सेट करें |
| tokens सिर्फ़ दूसरी attempt पर ही अस्वीकार होते हैं | पहली attempt वाला token आगे ले जाया गया था | हल उसी attempt के अंदर करें जो सबमिट करती है |
| किसी ApiException के अंदर ERROR_WRONG_USER_KEY | worker के environment में CAPSKIP_API_KEY सेट नहीं है | जहाँ worker चलता है वहाँ वह variable सेट करें, फिर उस worker को restart करें |
| हाथ से लिखे पोल से CAPCHA_NOT_READY | नतीजा पूरा होने से पहले ही पढ़ लिया गया | client को poll करने दें। वह ख़ुद ही धीमा होता जाता है |
यह आख़िरी रिस्पॉन्स वैसे ही लिखा जाता है जैसा दिखता है, और उसमें गायब अक्षर हमारी तरफ़ की गलती नहीं है, क्योंकि API सचमुच इसी वर्तनी में जवाब लौटाता है। इसकी पूरी व्याख्या यहाँ है: CAPCHA_NOT_READY गाइड.
FAQ
क्या मेरे workers सॉल्वर से अलग मशीनों पर चल सकते हैं?
हाँ, और एक process से आगे बढ़ते ही यही सामान्य सेटअप है। कनेक्शन सेटिंग्स में CapSkip को Server mode पर कर दें ताकि वह loopback के बजाय आपके नेटवर्क पते पर सुने, फिर हर worker के environment में CAPSKIP_HOST सेट करें। आपके अपने नेटवर्क पर वह पता एक LAN पता होता है और किसी चीज़ को इंटरनेट की तरफ़ खुला रखने की ज़रूरत नहीं। अगर कोई worker किसी VPS पर है, तो static पब्लिक IP इस्तेमाल करें और साथ में ऐसा firewall नियम रखें जो सिर्फ़ उन्हीं पतों को अनुमति दे जिनकी आप उम्मीद करते हैं।
मुझे असल में concurrency कितनी रखनी चाहिए?
दस से शुरू करें और दो चीज़ें देखते रहें: सॉल्वर मशीन, और यह कि आप जिस साइट पर सबमिट कर रहे हैं वह कैसे जवाब देती है। चूँकि हल await किया हुआ I/O है, इसलिए worker process ख़ुद शायद ही कभी सीमा बनता है। आम तौर पर साइट बनती है, और Node की जगह ख़त्म होने से बहुत पहले ही वह आपको rate limit करके बता देगी। यहाँ कुछ भी किसी बैलेंस के पीछे क़तार में नहीं लगता, इसलिए यह संख्या बजट का नहीं बल्कि क्षमता का फ़ैसला है।
जब मैंने attempts सेट ही नहीं किया, तो वही कैप्चा दो बार क्यों हल हुआ?
क्योंकि वह दोबारा चलना कोई retry नहीं था। stalled job attempts option की परवाह किए बिना फिर से queue में डाल दी जाती है, इस अनुमान पर कि उसे पकड़े हुए worker मर चुका है। इसकी वजह वह lock है जो समय पर renew नहीं हुआ, और ऐसा तब होता है जब processor await करने के बजाय CPU पकड़े रखता है। processor में synchronous काम ढूँढ़कर उसे हटा दें, तो डुप्लिकेट ख़त्म हो जाएगा।
क्या हल की अपनी अलग queue होनी चाहिए जिसे दूसरी jobs कॉल करें?
आम तौर पर नहीं। इसे अलग करने का मतलब है कि token एक queue की सीमा पार करता है और parent job के दोबारा शुरू होने तक Redis में पड़ा रहता है, और पहले ही एक्सपायर हो चुका token ख़र्च करने का यह सबसे तेज़ तरीक़ा है। हल और उसे इस्तेमाल करने वाले काम को एक ही processor में रखें, और credential के बजाय नतीजा लौटाएँ। अलग queue तभी समझ आती है जब वह जो लौटाए वह token हो ही नहीं।
संक्षेप में
worker concurrency बढ़ाएँ, क्योंकि एक बार में एक job वाला डिफ़ॉल्ट await किए हुए HTTP कॉल्स की queue को बेवजह रोके रखता है। processor को CPU से दूर रखें ताकि lock renew होता रहे, क्योंकि stalled job किसी दूसरे worker से दोबारा चलती है और उसकी क़ीमत आप बरबाद हुए throughput के रूप में चुकाते हैं। attempts कम रखें और किसी token को कभी एक attempt से दूसरी तक न ले जाएँ। जैसे ही कोई worker कहीं और रहने लगे, पता CAPSKIP_HOST में डालें और सॉल्वर को Server mode पर कर दें।
- क्लाइंट के पीछे मौजूद कच्चे एंडपॉइंट यहाँ दस्तावेज़ित हैं: CapSkip API डॉक्यूमेंटेशन.
- checkbox चैलेंज को ख़ुद यहाँ समझाया गया है: reCAPTCHA v2 सॉल्वर पेज.
वह concurrency संख्या चुनने से पहले एक बात तौलने लायक़ है: CapSkip एक असीमित कैप्चा सॉल्वर है जो आपके पास पहले से मौजूद हार्डवेयर पर चलता है, इसलिए इसे बढ़ाने में सिर्फ़ एक मशीन की क्षमता ख़र्च होती है, प्रति हल कुछ नहीं।
