Symfony Messenger वर्कर में कैप्चा कैसे हल करें (PHP)

Symfony Messenger में कैप्चा हल करने के लिए, पेज का पता रखने वाला एक छोटा message dispatch करें और हल उसके handler के अंदर करें। क्यू को एक ही SQLite फ़ाइल में रखें, ताकि Windows वर्कर को Redis या RabbitMQ की ज़रूरत न पड़े। इस Symfony Messenger कैप्चा सेटअप में चार जाल हैं, और यह गाइड हर एक को कवर करती है। जो exception रीट्राई के लिए ही बना लगता है, वह max_retries को नज़रअंदाज़ करता है। Messenger में किसी एक message के लिए कोई समय सीमा नहीं है। अगर redelivery timeout सबसे धीमे हल से छोटा हो, तो दूसरा वर्कर message को अपने हाथ में ले लेता है और दोबारा हल करता है। और pcntl extension के बिना, जो Windows वाले PHP में नहीं है, Ctrl+C वर्कर को हल के बीच में ही kill कर देता है।
आपको क्या चाहिए
- किसी Windows PC पर चल रहा CapSkip, जिसमें reCAPTCHA सॉल्वर चालू हो।
- PHP 8.4.1 या उसके बाद का वर्ज़न और Composer। Symfony 8.1 को यही PHP वर्ज़न चाहिए, और नीचे का handler PHP 8.4 के HTML parser का इस्तेमाल करता है। सब कुछ Windows 11 पर PHP 8.4.15, Symfony 8.1.8, doctrine/dbal 4.5.0 और capskip/capskip 1.3.0 के साथ चलाया गया।
- php.ini में चालू की गई तीन PHP extensions: curl, pdo_sqlite और openssl। वर्कर सादे http टेस्ट पेज पर सिर्फ़ पहले दो के साथ चला, लेकिन https पेज लाने के लिए openssl चाहिए, और Composer को भी चाहिए जब वह पैकेज डाउनलोड करता है।
- Redis, RabbitMQ या डेटाबेस सर्वर की ज़रूरत नहीं। Doctrine transport क्यू को एक SQLite फ़ाइल में रखता है।
- सॉल्वर के लिए एक पता। Local मोड में CapSkip केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server मोड में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरी मशीन पर चल रहा वर्कर API के ज़रिए उसे कॉल कर सके। दोनों यहाँ सेट किए जाते हैं: कनेक्शन सेटिंग्स.
नीचे के माप एक ही PC पर चल रहे एक नकली (mock) CapSkip और एक नकली (mock) लॉगिन पेज से आए हैं, जिनके साथ असली PHP SDK और असली Symfony components इस्तेमाल हुए। SDK और Messenger से जुड़े समय, जैसे 120 सेकंड, आपकी अपनी साइट पर भी लागू होते हैं। नकली लॉगिन साइट के स्वीकार और अस्वीकार के नियम उसके अपने ही हैं।
चरण 1: प्रोजेक्ट बनाएँ
सबसे पहले php -v जाँचें, क्योंकि इस गाइड की हर कमांड आपके PATH वाले PHP का इस्तेमाल करती है। अगर पहले कोई पुराना PHP हो (जैसे XAMPP 8.0 के साथ आता है), तो composer create-project रुकता नहीं। वह एक नोट प्रिंट करता है कि सबसे नए skeleton को PHP 8.4 चाहिए, और फिर भी एक पुराना skeleton इंस्टॉल कर देता है: PHP 8.0.30 पर वह Symfony 6.0 था। शुरू करने से पहले PHP 8.4 वाले फ़ोल्डर को इस विंडो के PATH में सबसे आगे रखें।
# PowerShell, with PHP 8.4 first on the PATH composer create-project symfony/skeleton cs-worker cd cs-worker composer require symfony/messenger symfony/doctrine-messenger doctrine/doctrine-bundle symfony/http-client capskip/capskip --no-scripts Remove-Item config\packages\doctrine.yaml, config\packages\messenger.yaml
Composer पूछ सकता है कि recipes से Docker configuration शामिल करना है या नहीं। n जवाब दें, क्योंकि डिफ़ॉल्ट विकल्प एक PostgreSQL सर्विस के लिए compose.yaml जोड़ देता है, जिसका इस सेटअप में इस्तेमाल नहीं होता। दो और बातें मायने रखती हैं। Doctrine recipe एक doctrine.yaml लिखती है जो doctrine/orm पैकेज माँगती है, जबकि यह प्रोजेक्ट उसका इस्तेमाल नहीं करता, इसलिए cache:clear ऐसी error के साथ फेल हो जाता है जो कहती है कि doctrine/orm ज़रूरी है। Composer का no-scripts flag उस चरण को छोड़ देता है, और आख़िरी लाइन उन दोनों YAML फ़ाइलों को मिटा देती है जो recipes ने लिखी थीं। चरण 2 उन्हें PHP फ़ाइलों से बदल देता है, जिन्हें Symfony config/packages से बिल्कुल उसी तरह लोड करता है।
चरण 2: transport को एक SQLite फ़ाइल पर पॉइंट करें
recipe ने .env में जो DATABASE_URL और MESSENGER_TRANSPORT_DSN लाइनें जोड़ी थीं, उन्हें इनसे बदलें, और बाकी जोड़ें। password जैसी असली वैल्यू .env.local में रखें, जिसे Symfony skeleton Git से बाहर रखता है।
MESSENGER_TRANSPORT_DSN=doctrine://default DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" CAPSKIP_HOST=127.0.0.1 CAPSKIP_PORT=8080 CAPSKIP_API_KEY=YOUR_API_KEY SITE_PASSWORD=YOUR_PASSWORD
SQLite का path जानबूझकर %kernel.project_dir% से शुरू होता है। sqlite:///var/data.db जैसा relative path तब unable to open database file के साथ फेल हुआ जब कमांड C:\ से चलाई गई। ऐसे फ़ोल्डर से चलाने पर जिसमें अपनी var डायरेक्टरी थी, उसने वहीं चुपचाप दूसरा डेटाबेस बना दिया, इसलिए दो कमांड को दो क्यू दिखीं। handler CapSkip के variables Autowire के ज़रिए पढ़ता है, क्योंकि SDK उन्हें अपने आप नहीं पढ़ता। CAPSKIP_API_KEY तभी मायने रखता है जब आप CapSkip में API key validation चालू करें।
<?php
// config/packages/doctrine.php (replaces the recipe's doctrine.yaml, which asks for doctrine/orm)
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'doctrine' => [
'dbal' => ['url' => '%env(resolve:DATABASE_URL)%'],
],
]);transport की सेटिंग्स दूसरी फ़ाइल में जाती हैं।
<?php
// config/packages/messenger.php (replaces the recipe's messenger.yaml)
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'framework' => [
'messenger' => [
'failure_transport' => 'failed',
'transports' => [
'async' => [
'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%',
// Longer than the slowest handler, or a second worker takes the message over.
'options' => ['redeliver_timeout' => 900],
'retry_strategy' => ['max_retries' => 3, 'delay' => 30000, 'multiplier' => 2, 'max_delay' => 300000],
],
'failed' => 'doctrine://default?queue_name=failed',
],
'routing' => ['App\Message\SolveLogin' => 'async'],
],
],
]);तीन सेटिंग्स पर सबसे ज़्यादा बोझ है। retry strategy कोशिशों के बीच करीब 30, 60 और 120 सेकंड रुकती है, जिससे CapSkip को वापस आने का समय मिलता है। Messenger हर देरी को दोनों तरफ़ 10 प्रतिशत तक बदल देता है, और एक टेस्ट में 29.4, 57.0 और 117.9 सेकंड log हुए। डिफ़ॉल्ट तीन बार रीट्राई करते हैं, और शुरुआत एक सेकंड के अंतर से होती है। failed transport दिखने से ज़्यादा मायने रखता है: उसके बिना, जिस message की आख़िरी रीट्राई खर्च हो चुकी हो, वह मिटा दिया जाता है और खो जाता है। और redeliver_timeout को नीचे उसके अपने सेक्शन में समझाया गया है।
चरण 3: message लिखें
message में लॉगिन के इनपुट होते हैं, और सॉल्वर जो बनाता है उसमें से कुछ भी नहीं। reCAPTCHA token दो मिनट तक वैध रहता है और सिर्फ़ एक बार काम करता है, जैसा कि Google के दस्तावेज़ में बताया गया है। नकली साइट यह नियम लागू करती है। जिस message में token था, वह तुरंत चलने पर स्वीकार हुआ और 130 सेकंड बाद चलने पर ठुकरा दिया गया। जिस message में सिर्फ़ पता था, उसने दोबारा हल किया और स्वीकार हुआ। इसलिए handler पेज लाता है, हल करता है और सबमिट करता है, सब एक के बाद एक तुरंत।
<?php
// src/Message/SolveLogin.php
namespace App\Message;
final class SolveLogin
{
public readonly string $jobId;
public function __construct(
public readonly string $siteUrl,
public readonly string $username,
) {
// Stays the same across retries: the message is serialized once and re-sent as it is.
$this->jobId = bin2hex(random_bytes(8));
}
}job id एक ही बार बनता है, जब message बनाया जाता है, और रीट्राई के दौरान वही रहता है क्योंकि Messenger वही serialize किया हुआ message दोबारा भेजता है। इस गाइड के अंत के पास वाला ledger इसी पर निर्भर है। Messenger message को PHP serialize से सहेजता है, इसलिए message क्यू में रहते हुए इस class का नाम बदलने से वे rows टूट जाती हैं।
चरण 4: तय करें कि किसे रीट्राई करना है
CapSkip चार तरह के exception उठाता है, और उनमें से हर एक दोबारा कोशिश के लायक नहीं है। ValidationException का मतलब है कि कॉल खुद गलत है और अगली बार भी उसी तरह फेल होगी। ApiException को रीट्राई करना सिर्फ़ ERROR_CAPTCHA_UNSOLVABLE के लिए ठीक है, जिसके ज़रिए CapSkip बताता है कि यह हल सफल नहीं हुआ। NetworkException (CapSkip चल नहीं रहा) और TimeoutException को रीट्राई करना ठीक है। अगर आपने CapSkip में Error Replacement टेक्स्ट बदला है, तो ERROR_CAPTCHA_UNSOLVABLE वाला मिलान उसे देख नहीं सकता, और वे नाकामियाँ रीट्राई नहीं होतीं।
<?php
// src/CapSkipFailure.php
namespace App;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\ValidationException;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
final class CapSkipFailure
{
public static function toMessenger(CapSkipError $e): \Throwable
{
// A bad parameter or an API error other than "unsolvable" will fail the same way next time.
$retry = !$e instanceof ValidationException
&& (!$e instanceof ApiException || str_contains($e->getMessage(), 'ERROR_CAPTCHA_UNSOLVABLE'));
// A plain exception is retried by the retry strategy, which also decides the delay and the limit.
return $retry
? new \RuntimeException($e->getMessage(), 0, $e)
: new UnrecoverableMessageHandlingException($e->getMessage(), 0, $e);
}
}सादे exception को retry strategy रीट्राई करती है, max_retries तक। UnrecoverableMessageHandlingException रीट्राई को छोड़कर सीधे failed transport में जाता है: टेस्ट में उसने एक ही कोशिश की और Removing from transport after 0 retries log किया।
यहाँ RecoverableMessageHandlingException का सहारा न लें, भले ही उसका नाम यही सुझाता हो। Symfony 8.1.8 में वह max_retries कुछ भी कहे, रीट्राई करता है, जब तक आप उसके पाँचवें argument के रूप में false पास न करें। max_retries के 3 होने पर उसने एक टेस्ट में 14 सेकंड में 7 कोशिशें कीं, और message उसके बाद भी क्यू में था। 7.4 के source में ऐसा कोई argument नहीं है और वह हमेशा रीट्राई करता है। सादा exception दोनों पर एक जैसा व्यवहार करता है।
चरण 5: handler लिखें
handler लॉगिन पेज को Symfony के HttpClient से लाता है, CSRF token और sitekey पढ़ता है, CapSkip से token माँगता है और फॉर्म पोस्ट करता है। इस सेटअप में HttpClient के पास कोई cookie jar नहीं है, इसलिए handler पहला Set-Cookie header हाथ से पोस्ट में कॉपी करता है। Django पर वही cookie होती है जो CSRF token के साथ आती है; अगर आपकी साइट और cookies सेट करती है, तो कई को जोड़ दें। वह Referer भी भेजता है, क्योंकि Django जैसे कुछ frameworks HTTPS पर बिना Referer वाली फॉर्म पोस्ट ठुकरा देते हैं। paths और फ़ील्ड नामों की जगह अपनी साइट वाले इस्तेमाल करें।
<?php
// src/MessageHandler/SolveLoginHandler.php
namespace App\MessageHandler;
use App\CapSkipFailure;
use App\Message\SolveLogin;
use CapSkip\CapSkip;
use CapSkip\Exceptions\CapSkipError;
use Dom\HTMLDocument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsMessageHandler]
final class SolveLoginHandler
{
private CapSkip $solver;
public function __construct(
private HttpClientInterface $http,
#[Autowire(env: 'CAPSKIP_HOST')] string $host,
#[Autowire(env: 'int:CAPSKIP_PORT')] int $port,
#[Autowire(env: 'CAPSKIP_API_KEY')] string $apiKey,
#[Autowire(env: 'SITE_PASSWORD')] private string $password,
) {
$this->solver = new CapSkip(['apiKey' => $apiKey, 'host' => $host, 'port' => $port]);
}
public function __invoke(SolveLogin $message): void
{
$url = $message->siteUrl . '/login';
$page = $this->http->request('GET', $url);
$cookie = explode(';', $page->getHeaders()['set-cookie'][0])[0];
$doc = HTMLDocument::createFromString($page->getContent(), LIBXML_NOERROR);
$csrf = $doc->querySelector('input[name=csrfmiddlewaretoken]')->getAttribute('value');
$sitekey = $doc->querySelector('.g-recaptcha')->getAttribute('data-sitekey');
try {
$token = $this->solver->recaptcha($sitekey, $url)['code'];
} catch (CapSkipError $e) {
throw CapSkipFailure::toMessenger($e);
}
$reply = $this->http->request('POST', $url, [
'headers' => ['Referer' => $url, 'Cookie' => $cookie],
'body' => ['csrfmiddlewaretoken' => $csrf, 'username' => $message->username,
'password' => $this->password, 'g-recaptcha-response' => $token],
]);
if ($reply->getStatusCode() >= 400) {
throw new \RuntimeException('login rejected: ' . $reply->getContent(false));
}
// Save the result here (your own code).
}
}recaptcha method एक array लौटाता है जिसकी code entry ही token है। दूसरे कैप्चा टाइप के लिए, मेल खाता SDK method कॉल करें और बाकी सब वैसा ही रखें। PHP में reCAPTCHA v2 हल करने की गाइड क्यू के बाहर वही कॉल कवर करती है।
handler status 400 और उससे ऊपर को नाकामी मानता है। जो साइट गलत password पर status 200 और दोबारा वही लॉगिन फॉर्म लौटाती है, उसके लिए पेज की खुद जाँच चाहिए, वरना नीचे आने वाला ledger जॉब को पूरी हुई दर्ज कर देगा। ठुकराया गया लॉगिन यहाँ सादा exception फेंकता है, इसलिए उसे बाकी की तरह रीट्राई किया जाता है, और गलत password पर चार हल खर्च होते हैं। जिस भी response के बारे में आप जानते हैं कि वह अंतिम है, उसके लिए UnrecoverableMessageHandlingException फेंकें।
चरण 6: message dispatch करें और वर्कर शुरू करें
एक छोटी console कमांड क्यू में एक message डालती है। Symfony 8 की console कमांड सादी invokable classes हो सकती हैं, जैसा यहाँ है।
<?php
// src/Command/SolveLoginCommand.php
namespace App\Command;
use App\Message\SolveLogin;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Messenger\MessageBusInterface;
#[AsCommand(name: 'app:solve-login', description: 'Queue one SolveLogin message')]
final class SolveLoginCommand
{
public function __construct(private MessageBusInterface $bus)
{
}
public function __invoke(#[Argument] string $siteUrl, #[Argument] string $username): int
{
$this->bus->dispatch(new SolveLogin($siteUrl, $username));
return 0;
}
}एक लॉगिन को क्यू में डालें और वर्कर शुरू करें। transport पहली बार इस्तेमाल होने पर अपनी table बना लेता है। https://example.com की जगह अपना कोई ऐसा पेज रखें जिसमें reCAPTCHA हो: placeholder के साथ पेज लाना फेल हो जाता है, और जॉब रीट्राई लूप में चली जाती है।
php bin/console app:solve-login https://example.com YOUR_USERNAME php bin/console messenger:consume async -vv --time-limit=3600 --memory-limit=128M
-vv के बिना वर्कर हर message पर लगभग कुछ भी प्रिंट नहीं करता, और Symfony के console log में timestamps नहीं होते, इसलिए ज़रूरत हो तो अपने जोड़ लें। memory limit सुरक्षित है: वर्कर करीब 12 MB पर रहा। time limit के बारे में चेतावनी चाहिए, जो आगे आती है।
एक message के लिए कोई समय सीमा नहीं है
Messenger में प्रति-message कोई timeout नहीं है। वर्कर का time limit ऑप्शन सिर्फ़ messages के बीच जाँचा जाता है, इसलिए वह किसी handler को बीच में नहीं काट सकता: क्यू में दो messages, 20 सेकंड के हल और 5 सेकंड के time limit के साथ, वर्कर 23.1 सेकंड ज़िंदा रहा, उसने पहला message पूरा किया और दूसरे को क्यू में छोड़ दिया। max_execution_time सेट करने से भी मदद नहीं मिलती। command-line PHP, php.ini की वैल्यू को नज़रअंदाज़ करता है, और उसे -d ऑप्शन या set_time_limit से मजबूर करने पर सिर्फ़ तब fatal error आई जब अटकी हुई कॉल पहले ही लौट चुकी थी, यानी 120.1 सेकंड पर।
अटके हुए हल की सीमा खुद SDK तय करता है। PHP SDK हर HTTP कॉल को 30 सेकंड की connect सीमा और 120 सेकंड की कुल सीमा देता है, और उन्हें बदलने का कोई ऑप्शन नहीं देता। जिस CapSkip ने कनेक्शन स्वीकार किया और submit का कभी जवाब नहीं दिया, उसमें वर्कर के 120.0 सेकंड लगे, फिर NetworkException उठा। जिसने टास्क ले लिया और फिर नतीजे के पोल का कभी जवाब नहीं दिया, उसमें 362.1 सेकंड लगे। SDK हर चुप पोल को अभी तैयार नहीं मानता, इसलिए recaptchaTimeout, यानी 300 सेकंड, खत्म हो चुका है यह देखने से पहले 120-120 सेकंड के तीन पोल बीत चुके थे, और तब उसने TimeoutException उठाया।
डेडलाइन से ठीक पहले चुप हो जाने वाला पोल उससे आगे पूरे 120 सेकंड तक चल सकता है। recaptchaTimeout के 20 होने पर, 14 सेकंड बाद चुप हुए CapSkip ने हल को 142.8 सेकंड पर खत्म किया। इनमें से हर एक ऐसे exception पर खत्म होता है जिसे चरण 4 की मैपिंग रीट्राई करती है। ये उस संख्या को तय करते हैं जिसके हिसाब से योजना बनानी है: डिफ़ॉल्ट सेटिंग्स के साथ एक handler को ज़्यादा से ज़्यादा करीब 9 मिनट लग सकते हैं, यानी submit के लिए 120 सेकंड, polling के लिए 300 और आख़िरी पोल के लिए 120, साथ में पेज लाना और पोस्ट।
redeliver_timeout से तय होता है कि आप दो बार हल करेंगे या नहीं
Doctrine transport message को delivered के रूप में चिह्नित करता है और handler के खत्म होने तक उसे table में रखता है। अगर कोई message redeliver_timeout से ज़्यादा देर से delivered है, तो कोई भी वर्कर उसे फिर से ले सकता है, चाहे पहला वर्कर अब भी काम कर रहा हो या नहीं। डिफ़ॉल्ट 3600 सेकंड है।
एक टेस्ट में, जहाँ redeliver_timeout 5 सेकंड था और हल में 15 लगे, दूसरे वर्कर को वही message पहले के 5.6 सेकंड बाद मिला। CapSkip ने दो बार हल किया और साइट ने दो पोस्ट स्वीकार कीं। टेस्ट साइट की एक-बार-इस्तेमाल वाली token जाँच ने इसे नहीं रोका, क्योंकि हर वर्कर ने अपना-अपना token हल किया था; वह सिर्फ़ तब ठुकराती है जब एक ही token दो बार इस्तेमाल हो। जब दूसरा वर्कर खत्म हुआ, तो उसके acknowledgement ने कोई row नहीं मिटाई और कोई error नहीं उठाई। timeout 40 सेकंड रखने पर उसी टेस्ट में एक हल और एक पोस्ट हुई।
इसलिए यह वैल्यू आपके सबसे धीमे handler से लंबी होनी चाहिए। ऊपर की SDK सीमाओं के साथ एक handler को करीब 9 मिनट लग सकते हैं, इसलिए चरण 2 की messenger.php फ़ाइल 900 सेकंड सेट करती है। इसकी कीमत यह है कि जिस message को कोई क्रैश हुआ वर्कर थामे है, वह दूसरे वर्कर के उठाने से पहले उतनी देर इंतज़ार करता है। कम वैल्यू एक सुरक्षा जाल है, फ़िक्स नहीं। एक टेस्ट में, जहाँ पहले वर्कर का हल अटक गया था, 20 सेकंड के timeout ने दूसरे वर्कर को काम अपने हाथ में लेकर पूरा करने दिया। अटका हुआ वर्कर फिर 120 सेकंड पर फेल हुआ और उसने अपनी रीट्राई शेड्यूल कर ली, इसलिए जॉब दूसरी बार हल हुई और साइट ने दो पोस्ट स्वीकार कीं।
हल गँवाए बिना वर्कर को रोकें
Messenger सामान्यतः pcntl के ज़रिए SIGTERM और SIGINT सुनता है, इसलिए Ctrl+C का मतलब है कि message पूरा करो, फिर रुको। Windows पर PHP में pcntl नहीं है। एक असली Ctrl+C event ने वर्कर को हल के बीच में 0.01 सेकंड के अंदर kill कर दिया, exit code 0xC000013A के साथ और बिना किसी log लाइन के। message अपने delivered समय के साथ table में बना रहा, इसलिए डिफ़ॉल्ट timeout के साथ कोई दूसरा वर्कर उसे एक घंटे तक नहीं देखता।
redeliver_timeout 10 सेकंड होने पर, दूसरे वर्कर को message क्यू में डाले जाने के करीब 22 सेकंड बाद मिला और उसने उसे फिर से हल किया, जिससे पहला हल बेकार चला गया।
फ़िक्स एक listener है जो वर्कर शुरू होते ही Windows console handler इंस्टॉल कर देता है।
<?php
// src/EventListener/WindowsGracefulStop.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
#[AsEventListener]
final class WindowsGracefulStop
{
public function __invoke(WorkerStartedEvent $event): void
{
// Without pcntl, Ctrl+C kills the process at once. This turns it into "finish the message, then stop".
if (function_exists('sapi_windows_set_ctrl_handler')) {
$worker = $event->getWorker();
sapi_windows_set_ctrl_handler(static fn () => $worker->stop());
}
}
}इसके साथ, Ctrl+C ने Stopping worker log किया, वर्कर ने हल और पोस्ट पूरी की, message को acknowledge किया और करीब 15 सेकंड बाद code 0 के साथ exit हो गया। keepalive ऑप्शन Windows पर मदद नहीं करता: उसे pcntl चाहिए और वह Signals are not supported के साथ रुक जाता है। messenger:stop-workers कमांड दो प्रोसेस के बीच काम करती है। एक खाली वर्कर 0.3 सेकंड में रुक गया, और एक व्यस्त वर्कर ने पहले अपना message पूरा किया और अगले को क्यू में छोड़ दिया।
listener की एक कीमत है। Ctrl+C अब अटके हुए हल को बीच में नहीं रोकता, क्योंकि उसका मतलब अब इस message के बाद रुकना है, और उसे दोबारा दबाने से exit मजबूर नहीं होता। एक टेस्ट में, दो-दो सेकंड के अंतर पर चार बार दबाने के बाद भी 20 सेकंड के हल के खत्म होने का इंतज़ार हुआ। अगर CapSkip चुप हो गया है, तो यह कई मिनट हो सकता है, इसलिए जब प्रोसेस को तुरंत हटाना हो, तो उसे Task Manager से खत्म करें।
वर्कर को चलता रखें
वर्कर को बीच-बीच में exit करने के लिए ही बनाया गया है, क्योंकि time limit और memory limit ऑप्शन उसे जानबूझकर रोक देते हैं। PowerShell का एक लूप हर बार नया वर्कर शुरू करता है। वह वर्कर को cmd के ज़रिए चलाता है ताकि log फ़ाइल सादा टेक्स्ट रहे; Windows PowerShell 5.1 में किसी native कमांड को redirect करने पर UTF-16 लिखा जाता है और error का शोर जुड़ जाता है।
while ($true) {
cmd /c "php bin/console messenger:consume async -vv --time-limit=3600 --memory-limit=128M >> var\worker.log 2>&1"
Start-Sleep -Seconds 5
}टेस्टिंग में, लूप ने time-limit के कारण हुए exit के बाद और प्रोसेस के kill होने के बाद नया वर्कर शुरू किया। लूप को sign in पर शुरू करना, जैसे Task Scheduler से या किसी सर्विस के रूप में, यहाँ टेस्ट नहीं किया गया।
एक ही जॉब को दो बार हल न करें
रीट्राई पूरे handler को दोहराती हैं। अगर फॉर्म पोस्ट सफल हो जाए और बाद का कोई चरण exception फेंके, जैसे नतीजा सेव करते समय, तो अगली कोशिश फिर से हल करती है और फिर से पोस्ट करती है। एक टेस्ट में, जहाँ सेव वाला चरण पोस्ट के बाद हमेशा फेल होता था, चरण 5 वाले handler ने एक message के लिए 4 हल और 4 स्वीकार हुई पोस्ट कीं।
एक छोटा ledger इसे ठीक कर देता है। message का job id पोस्ट के तुरंत बाद एक table में चला जाता है, और जो रीट्राई वह id पा लेती है, वह हल को छोड़ देती है और सिर्फ़ वही हिस्सा चलाती है जो फेल हुआ था।
<?php
// src/JobLedger.php (SQLite syntax: INSERT OR IGNORE)
namespace App;
use Doctrine\DBAL\Connection;
final class JobLedger
{
public function __construct(private Connection $db)
{
$this->db->executeStatement('CREATE TABLE IF NOT EXISTS job_done (job_id TEXT PRIMARY KEY, status INTEGER NOT NULL)');
}
public function status(string $jobId): ?int
{
$status = $this->db->fetchOne('SELECT status FROM job_done WHERE job_id = ?', [$jobId]);
return $status === false ? null : (int) $status;
}
public function done(string $jobId, int $status): int
{
$this->db->executeStatement('INSERT OR IGNORE INTO job_done (job_id, status) VALUES (?, ?)', [$jobId, $status]);
return $status;
}
}handler का दूसरा वर्ज़न पहले ledger से पूछता है। चरण 5 की फ़ाइल को इससे बदल दें।
<?php
// src/MessageHandler/SolveLoginHandler.php (second version: runs the login once per job)
namespace App\MessageHandler;
use App\CapSkipFailure;
use App\JobLedger;
use App\Message\SolveLogin;
use CapSkip\CapSkip;
use CapSkip\Exceptions\CapSkipError;
use Dom\HTMLDocument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsMessageHandler]
final class SolveLoginHandler
{
private CapSkip $solver;
public function __construct(
private HttpClientInterface $http,
private JobLedger $ledger,
#[Autowire(env: 'CAPSKIP_HOST')] string $host,
#[Autowire(env: 'int:CAPSKIP_PORT')] int $port,
#[Autowire(env: 'CAPSKIP_API_KEY')] string $apiKey,
#[Autowire(env: 'SITE_PASSWORD')] private string $password,
) {
$this->solver = new CapSkip(['apiKey' => $apiKey, 'host' => $host, 'port' => $port]);
}
public function __invoke(SolveLogin $message): void
{
// A retry after the POST went through skips the solve and the POST and repeats only the cheap part.
$status = $this->ledger->status($message->jobId)
?? $this->ledger->done($message->jobId, $this->login($message));
// Save the result here (your own code), using $status.
}
private function login(SolveLogin $message): int
{
$url = $message->siteUrl . '/login';
$page = $this->http->request('GET', $url);
$cookie = explode(';', $page->getHeaders()['set-cookie'][0])[0];
$doc = HTMLDocument::createFromString($page->getContent(), LIBXML_NOERROR);
$csrf = $doc->querySelector('input[name=csrfmiddlewaretoken]')->getAttribute('value');
$sitekey = $doc->querySelector('.g-recaptcha')->getAttribute('data-sitekey');
try {
$token = $this->solver->recaptcha($sitekey, $url)['code'];
} catch (CapSkipError $e) {
throw CapSkipFailure::toMessenger($e);
}
$reply = $this->http->request('POST', $url, [
'headers' => ['Referer' => $url, 'Cookie' => $cookie],
'body' => ['csrfmiddlewaretoken' => $csrf, 'username' => $message->username,
'password' => $this->password, 'g-recaptcha-response' => $token],
]);
if ($reply->getStatusCode() >= 400) {
throw new \RuntimeException('login rejected: ' . $reply->getContent(false));
}
return $reply->getStatusCode();
}
}ledger के साथ, उसी फेल होते सेव चरण ने 1 हल और 1 पोस्ट की, और रीट्राई सीधे सेव पर गईं। ledger दो वर्कर के एक ही जॉब को एक ही समय पर चलाने से नहीं बचाता। वह सुरक्षा सिर्फ़ काफ़ी लंबा redeliver_timeout देता है। job_done table में हर जॉब की एक row बनती है, इसलिए पुरानी rows समय-समय पर मिटाते रहें।
CapSkip को किसी दूसरी मशीन पर चलाना
वर्कर और सॉल्वर का एक ही PC पर होना ज़रूरी नहीं। कनेक्शन सेटिंग्स में CapSkip को Server mode पर कर दें, तो वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है। फिर .env या .env.local में CAPSKIP_HOST को उसी पते पर सेट करें।
अगर रास्ता इंटरनेट से होकर जाता है तो स्टैटिक पब्लिक IP इस्तेमाल करें, और API key validation चालू करें, CAPSKIP_API_KEY में भी वही key रखें। Server mode शुरू होते ही CapSkip अपना पोर्ट हर पते के लिए खोल देता है, इसलिए पहुँच को सिर्फ़ अपेक्षित मशीनों तक सीमित रखने के लिए बाकी पतों के लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर अब भी आपकी अपनी Windows मशीन है, और हल अब भी बिना मीटर वाले हैं।
Symfony Messenger में कैप्चा की आम errors
| आप जो देखते हैं | कारण | फिक्स |
|---|---|---|
| No transport supports Messenger DSN doctrine://default | doctrine-bundle गायब है, जबकि error में doctrine-messenger का नाम आता है | doctrine/doctrine-bundle को require करें |
| An exception occurred in the driver: could not find driver | pdo_sqlite बंद है, या recipe का PostgreSQL DATABASE_URL अब भी .env में है। दोनों में error का टेक्स्ट एक जैसा आता है | extension चालू करें और URL बदलें |
| no such table: messenger_messages | DSN में अब भी recipe का auto_setup=0 है, इसलिए table कोई नहीं बनाता | auto_setup=0 हटाएँ, या messenger:setup-transports चलाएँ |
| unable to open database file | SQLite का relative path | path की शुरुआत %kernel.project_dir% से करें |
| The doctrine/orm package is required when the doctrine.orm config is set | recipe की doctrine.yaml अब भी मौजूद है | उसे मिटाएँ और चरण 2 की PHP फ़ाइल इस्तेमाल करें |
| You have requested a non-existent parameter | doctrine.yaml मिटा दी गई और उसकी जगह कोई doctrine.php नहीं आई | doctrine.php फ़ाइल जोड़ें |
| Invalid Messenger routing configuration: class not found | message class की फ़ाइल गायब है या उसका namespace गलत है | चरण 3 की तरह src/Message/SolveLogin.php बनाएँ |
| Unknown option found in DSN: [redeliver_timout] | ऑप्शन की स्पेलिंग गलत है; error का संदेश सही ऑप्शन गिनाता है | स्पेलिंग ठीक करें |
| Removing from transport after 3 retries, और फिर message गायब हो जाता है | कोई failed transport configure नहीं है | चरण 2 की तरह failure_transport सेट करें |
| max_retries से ज़्यादा रीट्राई | RecoverableMessageHandlingException इस्तेमाल हो रहा है | चरण 4 की तरह सादा exception फेंकें |
| Ctrl+C पर वर्कर बिना किसी log लाइन के exit हो जाता है | Windows पर pcntl नहीं है | console handler listener जोड़ें |
| वही लॉगिन दो बार पोस्ट हुआ | redeliver_timeout handler से छोटा है, या पोस्ट के बाद का कोई चरण फेल हुआ | timeout बढ़ाएँ और ledger जोड़ें |
| Signals are not supported | keepalive ऑप्शन को pcntl चाहिए | Windows पर उस ऑप्शन को बंद रखें |
FAQ
क्या यह Symfony 7.4 LTS पर काम करता है?
ये blocks Symfony 8.1.8 और PHP 8.4.15 पर चलाए गए थे। Symfony 7.4 को PHP 8.2 या उसके बाद का वर्ज़न चाहिए, और handler PHP 8.4 के HTML parser का इस्तेमाल करता है, इसलिए पुराने PHP पर आपको कोई दूसरा parser लगाना होगा। रीट्राई का डिज़ाइन वही है, और सादा exception दोनों वर्ज़न पर एक जैसा व्यवहार करता है। सिर्फ़ RecoverableMessageHandlingException अलग है, जैसा चरण 4 समझाता है। 7.4 का व्यवहार उसका source पढ़ने से आया है, उसे चलाने से नहीं।
जो message हर रीट्राई में फेल हो जाता है, उसका क्या होता है?
चरण 2 वाले failed transport के साथ वह उसी table में failed नाम की क्यू के तहत चला जाता है। उसे messenger:failed:show से पढ़ें, messenger:failed:retry से दोबारा भेजें, और messenger:failed:remove से हटा दें। रीट्राई कमांड handler को आपके टर्मिनल में चलाती है। failed transport के बिना ये कमांड मौजूद ही नहीं होतीं, और message मिटा दिया जाता है। Messenger अंतिम नाकामी को critical के रूप में log करता है, Removing from transport after 3 retries लाइन के साथ।
क्या मैं एक से ज़्यादा वर्कर चला सकता हूँ?
हाँ। दूसरी विंडो में messenger:consume शुरू करें, और हर वर्कर अलग-अलग message लेता है। टेस्ट में दो वर्कर एक ही SQLite फ़ाइल पर बिना errors के चले। इसके ऊपर CapSkip की अपनी सीमा लागू होती है: उसका reCAPTCHA सॉल्वर डिफ़ॉल्ट रूप से एक साथ 10 टास्क चलाता है, और बाकी अपनी बारी का इंतज़ार करते हैं।
यह Laravel क्यू से कैसे अलग है?
Laravel का queue:work प्रति-जॉब timeout देता है, लेकिन उसे pcntl चाहिए, इसलिए Windows पर उसे चुपचाप नज़रअंदाज़ कर दिया जाता है, जैसा Laravel क्यू में कैप्चा हल करने की गाइड में समझाया गया है। Messenger में किसी भी प्लेटफ़ॉर्म पर प्रति-जॉब timeout नहीं है। इसके जोखिम हैं रीट्राई वाला exception, redelivery timeout और signal handling का न होना।
संक्षेप में
Symfony Messenger कैप्चा वर्कर को सिर्फ़ एक handler से ज़्यादा चाहिए। token नहीं, इनपुट भेजें, और हल handler के अंदर करें। रीट्राई के लिए सादा exception फेंकें और रुकने के लिए UnrecoverableMessageHandlingException, और एक failed transport जोड़ें ताकि कुछ भी गायब न हो। redeliver_timeout को सबसे लंबे handler से ऊपर रखें, जो SDK की सीमाओं के साथ करीब 9 मिनट है, और Windows console handler जोड़ें ताकि Ctrl+C हल को पूरा होने दे। Symfony Messenger दस्तावेज़ में यहाँ इस्तेमाल हुआ हर ऑप्शन दर्ज है।
- कैप्चा के वे सभी टाइप जिन्हें PHP पैकेज संभालता है: PHP कैप्चा सॉल्वर पेज.
- checkbox वाला reCAPTCHA कैसे काम करता है और सॉल्वर क्या-क्या कवर करता है: reCAPTCHA v2 सॉल्वर पेज.
रोज़ कुछ सौ लॉगिन निपटाने वाली क्यू महीने में हज़ारों हल करती है। अपनी खुद की Windows मशीन पर चल रहे कैप्चा सॉल्वर से इन्हें चलाने का मतलब है कि यह संख्या कभी प्रति-हल बिल में नहीं बदलती।
