مقدمه
در این مقاله نحوهٔ پیکربندی بازتلاش (retry) برای درخواستهای HTTP هنگام استفاده از OkHttp در پروژههای وب اسکریپینگ را بررسی میکنیم. هدف این است که پس از خواندن مقاله، بتوانید با دو رویکرد رایج — استفاده از یک کتابخانهٔ آماده (Retry4j) و نوشتن یک wrapper سفارشی — رفتار بازتلاش را پیادهسازی، پیکربندی و بهینه کنید. همچنین نکات عملی دربارهٔ کنترل تعداد تلاشها، backoff، تشخیص صفحات بن (ban) و نکات امنیتی و performance را پوشش میدهیم.
روش اول: استفاده از کتابخانهٔ Retry4j
ایدهٔ کلی: به جای نوشتن منطق بازتلاش از صفر، از یک کتابخانهٔ تخصصی مثل retry4j استفاده میکنیم تا سیاستهای بازتلاش، listenerها و تابع backoff را بهسادگی تعریف کنیم. Retry4j امکان تعریف تعداد تکرار، تاخیر بین تلاشها و exponential backoff را فراهم میکند.
مثال زیر یک پیادهسازی ساده را نشان میدهد. ورودی: هیچ پارامتری؛ خروجی: متن بدنهٔ پاسخ در صورت موفقیت. اگر پاسخ دارای وضعیتهایی مثل 429 یا 5xx باشد، exception پرتاب میشود تا retry فعال شود.
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import com.evanlennick.retry4j.CallExecutor;
import com.evanlennick.retry4j.CallExecutorBuilder;
import com.evanlennick.retry4j.Status;
import com.evanlennick.retry4j.config.RetryConfig;
import com.evanlennick.retry4j.config.RetryConfigBuilder;
public class RetryWithRetry4j {
public static List badStatusCodes = Arrays.asList(429, 500, 502, 503, 504);
public static void main(String[] args) throws Exception {
OkHttpClient client = new OkHttpClient();
Callable makeRequest = () -> {
Request request = new Request.Builder()
.url("https://quotes.toscrape.com")
.build();
try (Response response = client.newCall(request).execute()) {
int statusCode = response.code();
if (badStatusCodes.contains(statusCode)) {
throw new Exception("Bad status code: " + statusCode);
}
String body = response.body().string();
System.out.println("Response body length: " + body.length());
}
return null;
};
RetryConfig config = new RetryConfigBuilder()
.retryOnAnyException()
.withMaxNumberOfTries(5)
.withDelayBetweenTries(10, ChronoUnit.SECONDS)
.withExponentialBackoff()
.build();
CallExecutor callExecutor = new CallExecutorBuilder()
.config(config)
.onFailureListener((Status s) -> System.out.println("Maximum number of retries reached."))
.afterFailedTryListener((Status s) -> System.out.println("Total tries: " + s.getTotalTries()))
.build();
callExecutor.execute(makeRequest);
}
}
توضیح بخشها:
- badStatusCodes: فهرستی از کدهای HTTP که باید باعث بازتلاش شوند (مثلاً 429، 5xx).
- makeRequest: یک Callable که درخواست را میسازد، اجرا میکند و در صورت مشاهده وضعیت نامطلوب، استثنا پرتاب میکند.
- با RetryConfigBuilder پارامترهایی مانند تعداد تلاشها، تاخیر بین تلاشها و استفاده از withExponentialBackoff() تعیین میشود.
- CallExecutor اجرای واقعی را مدیریت میکند و listenerهایی مثل onFailureListener و afterFailedTryListener برای گزارش و مانیتورینگ تعریف میکنیم.
نکات عملی و محدودیتها:
- استفاده از retryOnAnyException() ساده است اما ممکن است باعث بازتلاش در مواردی شود که منطقی نیست (مثلاً خطاهای پارس کردن که نباید دوباره تلاش شوند). بهتر است شروط دقیقتر تعریف کنید.
- برای APIهایی که غیر ایندمپوتمنت (non-idempotent) هستند، بازتلاش خودکار میتواند باعث دوبار اجرای عملیات (مثل POST) شود؛ بهتر است برای درخواستهای GET/HEAD از retry استفاده کنید یا از شناسهٔ منحصر بهفرد برای جلوگیری از دوبار عملیات در سمت سرور کمک بگیرید.
- Exponential backoff همراه با محدودیت بالایی در حداکثر تاخیر کمک میکند که به سرور فشار وارد نشود.
روش دوم: نوشتن wrapper سفارشی (Custom Retry Logic)
ایدهٔ کلی: خودتان حلقهٔ retry را کنترل میکنید تا رفتار دقیقتری داشته باشید؛ مثلاً فقط روی خطاهای اتصال تلاش کنید، یا پس از مشاهده HTML مشخصی (مثلاً صفحهٔ بن) دوباره تلاش کنید. این رویکرد کنترل کامل بر جریان، گزارش و handling خطا را میدهد.
نمونهٔ سادهٔ wrapper با تعداد ثابت تلاشها:
import java.net.ConnectException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class CustomRetryLogic {
final public static int NUM_RETRIES = 3;
public static void main(String[] args) throws Exception {
Response response = null;
OkHttpClient client = new OkHttpClient();
for (int i = 0; i < NUM_RETRIES; i++) {
try {
Request request = new Request.Builder()
.url("https://quotes.toscrape.com")
.build();
response = client.newCall(request).execute();
int status = response.code();
if (status == 200 || status == 404) {
// خروج از حلقه در صورت موفقیت یا صفحهٔ پیدا نشد
break;
}
} catch (Exception e) {
boolean connectionError = e instanceof ConnectException;
if (connectionError) {
// لاگ، متریک یا backoff ساده
}
} finally {
System.out.println("Total tries: " + (i + 1));
}
// مثال: تاخیر افزایشی ساده بین تلاشها
Thread.sleep(2000L * (i + 1));
}
if (response != null && response.code() == 200) {
System.out.println("Response body: " + response.body().string());
} else {
System.out.println("No valid response after maximum number of tries");
}
}
}
توضیح مختصر:
- ورودی: ثابت NUM_RETRIES. خروجی: پاسخ موفق یا پیام خطا پس از تلاشهای متعدد.
- درون حلقه، در صورت گرفتن وضعیت موفق (200) یا 404 حلقه را متوقف میکنیم؛ برای سایر وضعیتها میتوانیم بازتلاش کنیم.
- در قسمت catch میتوان نوع خطا را بررسی و تصمیم مناسب (مثلاً عدم بازتلاش برای برخی خطاها) را اتخاذ کرد.
افزودن بررسی محتوای HTML (مثال با Jsoup)
گاهی سرور وضعیت 200 برمیگرداند اما محتوا صفحهٔ بن یا کپچا است. در این حالت بهتر است بعد از دریافت HTML، محتوای آن را بررسی کرده و در صورت پیدا شدن الگوهای بن، درخواست را مجدداً تلاش کنید.
import java.net.ConnectException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.Jsoup;
public class CustomRetryWithContentCheck {
final public static int NUM_RETRIES = 3;
public static void main(String[] args) throws Exception {
Response response = null;
String responseBodyText = null;
boolean validResponse = false;
OkHttpClient client = new OkHttpClient();
for (int i = 0; i < NUM_RETRIES; i++) {
try {
Request request = new Request.Builder()
.url("https://quotes.toscrape.com")
.build();
response = client.newCall(request).execute();
responseBodyText = response.body().string();
String pageTitle = Jsoup.parse(responseBodyText).title();
int status = response.code();
boolean validStatus = status == 200 || status == 404;
if (validStatus && !pageTitle.contains("Robot or human?")) {
validResponse = true;
break;
}
} catch (Exception e) {
boolean connectionError = e instanceof ConnectException;
if (connectionError) {
// لاگ یا هشدار
}
} finally {
System.out.println("Total tries: " + (i + 1));
}
// exponential-ish backoff ساده
Thread.sleep(1000L * (long) Math.pow(2, i));
}
if (response != null && validResponse && response.code() == 200) {
System.out.println("Response body: " + responseBodyText);
} else {
System.out.println("No valid response after maximum number of tries");
}
}
}
نکات مهم:
- بررسی title یا دیگر المانها (مثلاً وجود عبارتهایی مرتبط با کپچا یا بن) میتواند قابل اطمینانتر از تکیه صرف بر status code باشد.
- خواندن کل بدنهٔ پاسخ (response.body().string()) باعث مصرف حافظه میشود؛ برای صفحات خیلی بزرگ یا زمانی که فقط بخشی از محتوا لازم است، بهتر است استریم یا پارس جزئی انجام دهید.
بهترین روشها، performance و امنیت
- همیشه برای عملیات شبکه timeout مشخص کنید (connect/read/write timeouts) تا threadها برای مدت طولانی بلوکه نشوند.
- برای درخواستهای حساس به تکرار (مانند POST) از بازتلاش خودکار صرف نظر کنید یا mécanism idempotency اعمال کنید.
- Backoff: از exponential backoff همراه با ceiling (حداکثر تاخیر) استفاده کنید تا هم شانس موفقیت افزایش یابد و هم از overload سرور جلوگیری شود.
- اگر چند نخ یا سرویس همزمان درخواست میفرستید، هماهنگی در سیاست retry و محدود کردن نرخ (rate limiting) ضروری است.
- هنگام وب اسکریپینگ، استفاده از پراکسی، چرخش user-agent و رعایت robots.txt و سیاستهای سایت را مد نظر داشته باشید تا از بلاک شدن جلوگیری شود.
- مانیتورینگ و لاگینگ: ثبت تعداد retryها، زمانهای تاخیر و دلایل شکست برای تحلیل دلایل خطا حیاتی است.
مزایا و معایب دو رویکرد
- کتابخانهٔ آماده (Retry4j)
- مزایا: پیادهسازی سریع، امکانات listener و backoff آماده، خوانایی بهتر.
- معایب: حجم اضافهٔ وابستگی، ممکن است نیاز به سفارشیسازی عمیق داشته باشید.
- wrapper سفارشی
- مزایا: کنترل کامل، امکان تعریف قواعد دقیق برای retry بر اساس محتوای پاسخ.
- معایب: نیاز به نوشتن و نگهداری بیشتر، احتمال خطای انسانی در منطق retry.
چکلیست عملی برای پیادهسازی در پروژههای وب اسکریپینگ
- تصمیم بگیرید کدام درخواستها قابل بازتلاش هستند (معمولاً GET).
- کدهای HTTP و الگوهای محتوایی که باید بازتلاش شوند را فهرست کنید (مثلاً 429، 5xx، یا عبارتهای مشخص کپچا).
- الگوریتم backoff را مشخص کنید (exponential + ceiling) و حداکثر تعداد تلاش را تعیین کنید.
- timeou tهای مناسب برای اتصال و خواندن تعیین کنید.
- مکانیزم لاگینگ و متریکها را برای مانیتورینگ پیاده کنید.
- برای جلوگیری از بلوک شدن، از پراکسی، تاخیر تصادفی بین درخواستها و چرخش headerها استفاده کنید.
جمعبندی
برای افزایش مقاومت سیستم وب اسکریپینگ در برابر خطاهای شبکه و محدودیتهای سروری، بازتلاش هوشمند یکی از ابزارهای کلیدی است. اگر به دنبال پیادهسازی سریع و قابلپیکربندی هستید، استفاده از کتابخانهای مثل retry4j مناسب است؛ اما اگر نیاز به قواعد ویژهٔ تشخیص محتوا و کنترل کامل دارید، یک wrapper سفارشی بهتر پاسخگو خواهد بود. در هر دو حالت رعایت قوانین مربوط به backoff، مدیریت timeouts، کنترل لاگ و محدود کردن نرخ درخواستها ضروری است.





