mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-25 04:34:09 +00:00
feat(client): opt-in fasthttp transport (NewFastHTTPDoer)
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.
This commit is contained in:
+22
-4
@@ -215,6 +215,24 @@ func (b *Bot) buildRequest(ctx context.Context, method string, body io.Reader) (
|
||||
return req.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
// bufferReadCloser exposes a *bytes.Buffer as io.ReadCloser without going
|
||||
// through io.NopCloser. Keeping the concrete *bytes.Buffer accessible lets
|
||||
// alternative HTTPDoers (e.g. FastHTTPDoer) type-assert and pass the
|
||||
// underlying bytes through to their native body-set APIs without copying.
|
||||
type bufferReadCloser struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (bufferReadCloser) Close() error { return nil }
|
||||
|
||||
// readerReadCloser is the equivalent wrapper for *bytes.Reader (used by
|
||||
// the Marshal fallback path when the codec doesn't implement BodyEncoder).
|
||||
type readerReadCloser struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
func (readerReadCloser) Close() error { return nil }
|
||||
|
||||
// bodyToReadCloser wraps body for assignment to *http.Request.Body. The
|
||||
// type switch covers the body shapes encodeJSONBody returns: a pooled
|
||||
// *bytes.Buffer (BodyEncoder path or {} fast path) or a *bytes.Reader
|
||||
@@ -226,16 +244,16 @@ func bodyToReadCloser(body io.Reader) (io.ReadCloser, int64, func() (io.ReadClos
|
||||
case *bytes.Buffer:
|
||||
buf := v.Bytes()
|
||||
length := int64(len(buf))
|
||||
return io.NopCloser(v), length, func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(buf)), nil
|
||||
return bufferReadCloser{v}, length, func() (io.ReadCloser, error) {
|
||||
return readerReadCloser{bytes.NewReader(buf)}, nil
|
||||
}
|
||||
case *bytes.Reader:
|
||||
length := int64(v.Len())
|
||||
// Snapshot the reader's current data so GetBody returns a fresh one.
|
||||
snapshot := *v
|
||||
return io.NopCloser(v), length, func() (io.ReadCloser, error) {
|
||||
return readerReadCloser{v}, length, func() (io.ReadCloser, error) {
|
||||
s := snapshot
|
||||
return io.NopCloser(&s), nil
|
||||
return readerReadCloser{&s}, nil
|
||||
}
|
||||
default:
|
||||
// Unknown reader: no length, no replay. Should not happen with the
|
||||
|
||||
Reference in New Issue
Block a user