Files
miti99bot/internal/modules/coin/views.go
T
tiennm99 b7838f7347 fix(modules): fetch stats prices sequentially to reuse pooled connections
Parallelizing the per-symbol fetches opened N simultaneous TLS handshakes into
an empty connection pool. On the memory-constrained Lambda (256MB ~0.15 vCPU)
those CPU-bound handshakes thrashed and each exceeded the per-fetch timeout, so
every ticker rendered "(no price)". Sequential fetches reuse the price client's
keep-alive connection (one handshake), which is why the code was sequential by
design.

- revert stock and coin stats loops to sequential (keep the reply-budget
  sub-context and 3s per-fetch timeout)
- log per-symbol fetch failures instead of silently swallowing them
2026-06-25 14:28:25 +07:00

67 lines
2.3 KiB
Go

package coin
import (
"context"
"sort"
"strings"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/tiennm99/miti99bot/internal/log"
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
)
func (s *state) handleStats(ctx context.Context, b *bot.Bot, update *models.Update) error {
userID, ok := senderInfo(update)
if !ok {
return chathelper.Reply(ctx, b, update.Message, "Cannot identify user - /coin_stats needs a sender.")
}
p, err := LoadPortfolio(ctx, s.kv, userID, s.now().UnixMilli())
if err != nil {
log.Error("coin_load_portfolio", "user", userID, "err", err)
return chathelper.Reply(ctx, b, update.Message, "Could not load coin portfolio. Try again later.")
}
lines := []string{"Coin Account Summary\n", "USD: " + FormatUSD(p.USD)}
totalValue := p.USD
// Fetch sequentially (not concurrently) so the price client's keep-alive
// connection pool is reused across coins rather than opening N simultaneous
// TLS handshakes — the latter thrashes the CPU-constrained Lambda and times
// out. The reply-reserved sub-context bounds the whole loop so the final
// Reply keeps its budget; a slow/failed provider degrades to "(price
// unavailable)" instead of failing the summary.
fetchCtx, cancel := chathelper.FetchContext(ctx)
defer cancel()
for _, symbol := range sortedAssetSymbols(p.Assets) {
held := p.Assets[symbol]
line := symbol + ": " + FormatCoinQty(held)
if coin, err := ResolveCoinSymbol(symbol); err == nil {
if price, err := s.prices.FetchUSD(fetchCtx, coin); err == nil {
value := held * price.USD
totalValue += value
line += " = " + FormatUSD(value) + " @ " + FormatUSD(price.USD) + " (" + price.Source + ")"
} else {
log.Error("coin_fetch_price", "symbol", symbol, "err", err)
line += " (price unavailable)"
}
}
lines = append(lines, line)
}
lines = append(lines, "Total value: "+FormatUSD(totalValue))
lines = append(lines, "Invested: "+FormatUSD(p.Meta.Invested))
lines = append(lines, "P&L: "+FormatPnLUSD(totalValue, p.Meta.Invested))
return chathelper.Reply(ctx, b, update.Message, strings.Join(lines, "\n"))
}
func sortedAssetSymbols(assets map[string]float64) []string {
symbols := make([]string, 0, len(assets))
for symbol, amount := range assets {
if amount > 0 {
symbols = append(symbols, symbol)
}
}
sort.Strings(symbols)
return symbols
}