🔂 UnaryRetries (GoMT4)¶
Goal: apply a consistent retry pattern for unary RPC calls (quotes, account, orders) using helpers already present in this repo.
Real code refs:
- Backoff & helpers:
examples/mt4/MT4Account.go(waitWithCtx,backoffDelay,maxRetries)- Typical calls:
Quote,AccountSummary,OrderSend,OrderModify,OrderClose
✅ Principles¶
- Retry only transient transport errors:
codes.Unavailable(and network I/O errors if wrapped accordingly). - Use per‑call timeout via
context.WithTimeoutto bound latency. - Between attempts, sleep with backoff + jitter using
waitWithCtx(ctx, backoffDelay(attempt)). - Stop immediately on non‑transient (business) errors.
🧱 Skeleton helper (mirrors your account layer)¶
func (a *MT4Account) callWithRetry(ctx context.Context, fn func(context.Context) error) error {
var last error
for attempt := 0; attempt < maxRetries; attempt++ {
if err := fn(ctx); err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
// transient: back off and retry
if err := waitWithCtx(ctx, backoffDelay(attempt)); err != nil {
return err // context cancelled/deadline
}
last = err
continue
}
return err // non‑transient → bubble up
}
return nil // success
}
return fmt.Errorf("max retries reached: %w", last)
}
In this project, a similar logic is called directly from the account methods so as not to duplicate backups throughout the project.
⏱️ Per‑call timeout wrapper¶
func withTimeout(parent context.Context, d time.Duration, fn func(context.Context) error) error {
ctx, cancel := context.WithTimeout(parent, d)
defer cancel()
return fn(ctx)
}
💱 Example: robust Quote¶
err := withTimeout(ctx, 3*time.Second, func(c context.Context) error {
return a.callWithRetry(c, func(cc context.Context) error {
q, err := a.Quote(cc, symbol)
if err != nil { return err }
log.Printf("%s %.5f/%.5f @ %s", symbol, q.GetBid(), q.GetAsk(), q.GetTime().AsTime())
return nil
})
})
if err != nil { return fmt.Errorf("quote failed: %w", err) }
🧾 Example: AccountSummary health‑check¶
err := withTimeout(ctx, 3*time.Second, func(c context.Context) error {
return a.callWithRetry(c, func(cc context.Context) error {
_, err := a.AccountSummary(cc)
return err
})
})
if err != nil { return fmt.Errorf("health‑check failed: %w", err) }
🛒 Example: OrderSend (market)¶
err := withTimeout(ctx, 8*time.Second, func(c context.Context) error {
return a.callWithRetry(c, func(cc context.Context) error {
_, err := a.OrderSend(cc, symbol, side, volume, nil, &slip, sl, tp, &comment, &magic, nil)
return err
})
})
if err != nil { return fmt.Errorf("OrderSend failed: %w", err) }
🎛️ Tuning¶
Constants in examples/mt4/MT4Account.go:
const (
backoffBase = 300 * time.Millisecond
backoffMax = 5 * time.Second
jitterRange = 200 * time.Millisecond
maxRetries = 10
)
- VPS/LAN:
backoffBase=150ms,backoffMax=3–5s, timeouts 2–3s for reads. - Home/unstable:
backoffMax=8–10s, timeouts 4–6s (reads) / 6–10s (trades).
⚠️ Pitfalls¶
- We will delete business errors (for example, invalid volume/price) — no need, return it immediately.
- There is no
defer cancel()— goroutin leaks. - Too aggressive backoff — "pounding" on the network; increase the "backoffBase" and jitter spread.
- One global context for all — it is better to have a separate timeout for each call.
🔗 See also¶
HandleReconnect.md— for streaming and general strategy.- `Reliability (en)' — summary recommendations on timeouts/retreats.
GetQuote.md,PlaceMarketOrder.md— where it is applied live.