mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-15 12:15:05 +00:00
fix(modules): reserve reply budget and cap price-fetch timeouts in stats handlers
Stats handlers reused the single bounded update context for both upstream price fetches and the final Telegram reply, while per-upstream HTTP timeouts equalled the whole-handler budget. One slow upstream drained the deadline and the reply failed with "context deadline exceeded" (observed on /stock_stats). - add chathelper.FetchContext: fetches run under a child context that reserves a tail of the deadline for the reply, which is sent on the original context - cap kbs/coin/gold/vnappmob HTTP timeouts at 3s so one hung upstream cannot consume the reply budget - fetch held stock/coin prices concurrently (bounded) so total latency tracks the slowest single fetch instead of their sum; per-symbol failures degrade to a "no price" line and the summary still sends
This commit is contained in:
1 parent
eb8c2e0064
commit
958da76d33
12 files changed
+297
-34
No files matched your search
@@ -51,6 +51,31 @@ func ArgAfterCommand(text string) string {
|
||||
// NowMillis returns current UTC ms-since-epoch.
|
||||
func NowMillis() int64 { return time.Now().UTC().UnixMilli() }
|
||||
|
||||
// replyReserve is the slice of the handler's deadline kept aside for delivering
|
||||
// the Telegram reply. The whole update handler runs under one bounded context
|
||||
// (see internal/telegram/webhook.go); if upstream price fetches consume all of
|
||||
// it, the final SendMessage fails with "context deadline exceeded" and the user
|
||||
// sees no response. Reserving a fixed tail guarantees delivery headroom.
|
||||
const replyReserve = 3 * time.Second
|
||||
|
||||
// FetchContext derives a child of ctx for upstream data fetches, leaving
|
||||
// replyReserve of the parent's deadline for the subsequent Reply (which must be
|
||||
// called with the original ctx, not this child). If ctx has no deadline, or
|
||||
// less than replyReserve remains, the child gets a small positive floor so a
|
||||
// fetch still attempts rather than failing instantly. Callers must call the
|
||||
// returned cancel.
|
||||
func FetchContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
dl, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
return context.WithCancel(ctx)
|
||||
}
|
||||
budget := time.Until(dl) - replyReserve
|
||||
if budget < time.Second {
|
||||
budget = time.Second
|
||||
}
|
||||
return context.WithTimeout(ctx, budget)
|
||||
}
|
||||
|
||||
// Reply sends a plain-text response to the chat the inbound message came from.
|
||||
//
|
||||
// Forwards MessageThreadID so replies in a forum-supergroup topic stay in the
|
||||
|
||||
@@ -3,12 +3,53 @@ package chathelper
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
func TestFetchContext(t *testing.T) {
|
||||
t.Run("reserves reply budget from parent deadline", func(t *testing.T) {
|
||||
parent, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
child, childCancel := FetchContext(parent)
|
||||
defer childCancel()
|
||||
dl, ok := child.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("child has no deadline")
|
||||
}
|
||||
// budget = parent remaining (~10s) - replyReserve (3s) ≈ 7s.
|
||||
if d := time.Until(dl); d > 8*time.Second || d < 6*time.Second {
|
||||
t.Fatalf("fetch budget = %v, want ≈7s (10s parent - 3s reserve)", d)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("floors to 1s when parent deadline is within the reserve", func(t *testing.T) {
|
||||
parent, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
child, childCancel := FetchContext(parent)
|
||||
defer childCancel()
|
||||
dl, _ := child.Deadline()
|
||||
if d := time.Until(dl); d < 900*time.Millisecond || d > 1100*time.Millisecond {
|
||||
t.Fatalf("floored budget = %v, want ≈1s", d)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no parent deadline yields a cancelable child", func(t *testing.T) {
|
||||
child, childCancel := FetchContext(context.Background())
|
||||
defer childCancel()
|
||||
if _, ok := child.Deadline(); ok {
|
||||
t.Fatal("child unexpectedly has a deadline")
|
||||
}
|
||||
childCancel()
|
||||
if child.Err() == nil {
|
||||
t.Fatal("cancel did not propagate to child")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubjectFor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in new issue
Block a user