mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
bfb7e9875e
Adds an alternative HTTPDoer backed by valyala/fasthttp for high-throughput
bots. Cuts per-call allocs from 102 to 56 in the cross-library bench
(within 8 of telego, which uses fasthttp by default), and per-call bytes
from 11.1 KiB to 6.6 KiB.
bot := client.New(token,
client.WithHTTPClient(client.NewFastHTTPDoer()),
)
Implementation notes:
- Wraps *fasthttp.Client behind the existing HTTPDoer (Do *http.Request)
interface, so RetryDoer, custom transports, observability middleware,
and the 1428 generated tests all keep working as-is.
- Translates *http.Request -> fasthttp.Request once per call and
returns a *http.Response whose Body releases the pooled fasthttp
response on Close (net/http contract).
- Recognises the bufferReadCloser / readerReadCloser shapes produced
by buildRequest and passes their underlying bytes straight to
SetBodyRaw -- no io.ReadAll, no copy.
- Honours ctx.Deadline via DoDeadline, falls back to WithFastHTTPReadTimeout
when no deadline is set. fasthttp.ErrTimeout maps to
context.DeadlineExceeded for errors.Is compatibility.
Default stays net/http: fasthttp is HTTP/1.1 only, doesn't compose with
the http.RoundTripper middleware ecosystem, and most users don't have
the throughput to notice. Bots making thousands of API calls/sec should
opt in.
Multipart/file-upload path remains on net/http per the agreed scope --
the perf bottleneck was JSON-method round-trip, not file uploads.
Time numbers in the report deferred until a quiet-system bench run;
allocs/bytes numbers (which are deterministic per code path) are
already updated.
95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFastHTTPDoer_BasicRoundTrip(t *testing.T) {
|
|
got := make(chan struct{ method, ct, body string }, 1)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
got <- struct{ method, ct, body string }{r.Method, r.Header.Get("Content-Type"), string(body)}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"ok":true,"result":42}`)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewFastHTTPDoer()
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL+"/sendMessage", strings.NewReader(`{"chat_id":1,"text":"hi"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(context.Background())
|
|
|
|
resp, err := d.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status: got %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if string(body) != `{"ok":true,"result":42}` {
|
|
t.Fatalf("body: got %q", body)
|
|
}
|
|
|
|
rec := <-got
|
|
if rec.method != http.MethodPost {
|
|
t.Fatalf("method: got %q", rec.method)
|
|
}
|
|
if rec.ct != "application/json" {
|
|
t.Fatalf("content-type: got %q", rec.ct)
|
|
}
|
|
if rec.body != `{"chat_id":1,"text":"hi"}` {
|
|
t.Fatalf("body: got %q", rec.body)
|
|
}
|
|
}
|
|
|
|
func TestFastHTTPDoer_HonoursContextDeadline(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(200 * time.Millisecond)
|
|
_, _ = io.WriteString(w, "ok")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewFastHTTPDoer(WithFastHTTPReadTimeout(time.Hour))
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
|
defer cancel()
|
|
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
|
req = req.WithContext(ctx)
|
|
|
|
_, err := d.Do(req)
|
|
if err == nil {
|
|
t.Fatal("expected timeout error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestFastHTTPDoer_IntegratesWithBot(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = io.WriteString(w, `{"ok":true,"result":{"message_id":7,"date":0,"text":"hi"}}`)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
bot := New("123:abc",
|
|
WithBaseURL(srv.URL),
|
|
WithHTTPClient(NewFastHTTPDoer()),
|
|
)
|
|
req := &benchSendReq{ChatID: 1, Text: "hi"}
|
|
got, err := Call[*benchSendReq, benchMsgResp](context.Background(), bot, "sendMessage", req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.MessageID != 7 || got.Text != "hi" {
|
|
t.Fatalf("got %+v", got)
|
|
}
|
|
}
|