基于Broker API的订单状态获取策略优化咨询:每秒轮询可行性与重试机制缺陷解决方案
Great question—let's tackle your two core issues first, then walk through a refined approach that addresses your concerns better than the options you've considered so far.
Core Issue 1: Is Polling Every Second Feasible?
Short answer: It's feasible in a limited window, but not as a long-term strategy. Polling every second non-stop will indeed clutter your logs, increase the risk of hitting API rate limits (you already handle this error case), and waste unnecessary network/CPU resources. However, for the first few seconds after placing an order, short-interval polling makes sense because many orders confirm quickly, and users expect timely feedback. The key is to limit this aggressive polling to a narrow initial window, then switch to a more efficient strategy for slower orders.
Core Issue 2: Handling Long-Running Order Confirmations
Your current 3-retry limit is clearly too low for orders that take 15-20s+ to confirm. The problem with your proposed solutions is that they either cut off too early (fixed timeout) or introduce unnecessary delays (pure exponential backoff). The best approach here is a dual-stage polling system that combines immediate user feedback with persistent background processing.
Refined Solution: Dual-Stage Polling + Persistent Background Queue
Here's how to implement it:
Stage 1: Immediate, Short-Interval Polling (First 10-15 Seconds)
- For the first 10-15 seconds after the order is placed, poll every 1-2 seconds. This ensures users get fast updates for orders that confirm quickly, without excessive log spam.
- Set a time-based limit here (e.g., 15 seconds) instead of a retry count—this is more intuitive for handling variable confirmation times.
Stage 2: Background Queue with Gentle Exponential Backoff
- If the order status is still unresolved after Stage 1, add it to a persistent background queue (use something like Redis Queue, Celery with async support, or your framework's built-in task queue).
- Background workers will poll the order status using a gentle exponential backoff strategy: start with a 2-second interval, multiply by 1.5 each retry, cap the interval at 30 seconds, and set a total timeout of 5-10 minutes (adjust based on your broker's maximum expected confirmation time).
- When the worker finally gets a
ConfirmedorRejectedstatus, trigger yourstrategy_status_signalto update the user, and persist the final status to your database for future reference.
Modified Code Example
Here's how to adjust your get_order function to implement Stage 1, plus a pseudo-code snippet for the background queue worker:
import asyncio from datetime import datetime, timedelta async def get_order( subs_details: SubscriptionDetails, strike_unique_id: str, order_id: str ) -> tuple[str, float, str]: URL = conn.BALAJI_ORDER_STATUS headers = {"Content-Type": "application/json", "Authorization": conn.BALAJI_API_TOKEN} order_price = 0.0 order_status = 'NA' error = '' start_time = datetime.now() initial_poll_window = timedelta(seconds=15) # First 15s: aggressive polling initial_poll_interval = 1 # Poll every 1s try: (broker, token, strategy_id, activation_id) = extract_get_order_details(subs_details) payload = {"strike_id": strike_unique_id, "brokerName": broker, "token_id": token, "order_id": order_id} logger.info(f"Get Order payload: {payload}") # Stage 1: Immediate polling window while datetime.now() - start_time < initial_poll_window: async with SessionManager.session.post(URL, json=payload, headers=headers) as r: status_code = r.status res = await r.json() logger.info(f"Get order for {order_id} with response: {res}") if status_code != 200: if 'invalid order id' in res.get('message', '').lower(): order_status = "Invalid" error = "Invalid order ID" break await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'ERROR', 'Internal Error')) error = "API request failed" break order_log = res.get("order_log", {}) error = order_log.get("error_reason") or error order_price = order_log.get('order_price') or order_price order_status = order_log.get("order_status") or order_status # Handle rate limit: extend interval temporarily if f"API rate limit reached {broker}" in error: logger.info(f"Rate limit hit for {broker}, extending poll interval") await asyncio.sleep(initial_poll_interval * 2) continue # Check for final status if order_status == "Confirmed": logger.info(f"ORDER COMPLETED for {strike_unique_id} broker:{broker} & order_id:{order_id}") await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'SUCCESS', 'Order confirmed')) break elif order_status == "Rejected": await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'ERROR', error)) logger.info(f"ORDER REJECTED for {strike_unique_id} broker:{broker} & order_id:{order_id}") break await asyncio.sleep(initial_poll_interval) # If no final status after Stage 1, send to background queue if order_status not in ["Confirmed", "Rejected", "Invalid"]: logger.info(f"Order {order_id} not resolved in initial window, adding to background queue") # Replace with your actual queue enqueue logic (async-compatible) await background_queue.enqueue( poll_order_in_background, subs_details, strike_unique_id, order_id, strategy_id, activation_id ) except Exception as e: logger.error(f"Error while fetching order details: {e}") error = str(e) return order_status, order_price, error # Background worker function (runs in separate task/process) async def poll_order_in_background(subs_details, strike_unique_id, order_id, strategy_id, activation_id): URL = conn.BALAJI_ORDER_STATUS headers = {"Content-Type": "application/json", "Authorization": conn.BALAJI_API_TOKEN} attempt = 0 max_attempts = 20 # ~5 minutes total (2s → 3s → 4.5s ... capped at 30s) base_interval = 2 max_interval = 30 (broker, token, _, _) = extract_get_order_details(subs_details) payload = {"strike_id": strike_unique_id, "brokerName": broker, "token_id": token, "order_id": order_id} while attempt < max_attempts: try: async with SessionManager.session.post(URL, json=payload, headers=headers) as r: res = await r.json() order_log = res.get("order_log", {}) order_status = order_log.get("order_status") or 'NA' error = order_log.get("error_reason") or '' if order_status == "Confirmed": logger.info(f"BACKGROUND: ORDER COMPLETED for {order_id}") await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'SUCCESS', 'Order confirmed')) return elif order_status == "Rejected": logger.info(f"BACKGROUND: ORDER REJECTED for {order_id}") await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'ERROR', error)) return elif 'invalid order id' in res.get('message', '').lower(): logger.info(f"BACKGROUND: Invalid order ID {order_id}") await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'ERROR', 'Invalid order ID')) return # Calculate backoff interval interval = min(base_interval * (1.5 ** attempt), max_interval) await asyncio.sleep(interval) attempt += 1 except Exception as e: logger.error(f"BACKGROUND error polling order {order_id}: {e}") await asyncio.sleep(base_interval * 2) attempt += 1 # If max attempts reached logger.info(f"BACKGROUND: Max attempts exceeded for order {order_id}") await asyncio.create_task(strategy_status_signal(strategy_id, activation_id, 'ERROR', 'Order status timed out'))
Key Improvements
- Balanced User Feedback: Stage 1 ensures fast updates for quick orders, while Stage 2 handles slow ones without blocking the user flow.
- Rate Limit Resilience: Temporarily extends polling intervals when hitting rate limits, instead of just retrying.
- Persistent Processing: The background queue ensures orders aren't lost if your service restarts, and the gentle backoff avoids overwhelming the broker's API.
- Clear Termination Conditions: Stops polling immediately for invalid order IDs or final statuses, wasting no resources.
Future-Proofing
Once your broker supports WebSockets, you can replace the background queue with a WebSocket subscription for order IDs. This will eliminate polling entirely for those orders, making the system even more efficient.
内容的提问来源于stack exchange,提问作者Himanshu Sharma

