mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-07-09 01:43:00 +00:00
perf(transport): pool *bytes.Buffer + MaxBytesReader for webhook decode
Replace the hand-rolled make([]byte, 0, 1024) + make([]byte, 4096) read loop in WebhookServer.ServeHTTP with a sync.Pool-backed bytes.Buffer drained via ReadFrom, fronted by http.MaxBytesReader for the 1 MiB body cap. putWebhookBuf caps Cap() at 256 KiB before returning to the pool so a rare oversized update (max body is 1 MiB) doesn't permanently bloat the pool. Bench delta on Webhook_ServeHTTP: 2564ns -> 2020ns (-21%), 12707B -> 7648B (-40%), 24 -> 23 allocs. The big byte saving is the 4 KiB tmp buffer + 1 KiB initial buf cap, replaced by one reused buffer across requests. The remaining alloc count is dominated by codec.Unmarshal decoding Update's pointer fields (*string, *int64), which is downstream of this change.
This commit is contained in:
+33
-15
@@ -6,6 +6,7 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
@@ -18,6 +19,24 @@ import (
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
)
|
||||
|
||||
// webhookBufPool reuses *bytes.Buffer for incoming webhook bodies.
|
||||
// Webhook payloads are typically a single Telegram Update (commonly
|
||||
// 1-8 KiB), so a buffer that has grown once will satisfy most
|
||||
// subsequent requests with no additional allocation.
|
||||
var webhookBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
|
||||
|
||||
// maxWebhookBufCap caps the buffer size returned to webhookBufPool so
|
||||
// a rare oversized update doesn't permanently bloat the pool. Buffers
|
||||
// larger than this are dropped on the floor.
|
||||
const maxWebhookBufCap = 256 * 1024
|
||||
|
||||
func putWebhookBuf(buf *bytes.Buffer) {
|
||||
if buf.Cap() > maxWebhookBufCap {
|
||||
return
|
||||
}
|
||||
webhookBufPool.Put(buf)
|
||||
}
|
||||
|
||||
// WebhookServer implements Updater by exposing an http.Handler that
|
||||
// receives updates from Telegram. It can be mounted on the user's own
|
||||
// HTTP server (via ServeHTTP) or run standalone (via ListenAndServe).
|
||||
@@ -108,28 +127,27 @@ func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.handlers.Add(1)
|
||||
defer w.handlers.Done()
|
||||
|
||||
const maxBody = 1 << 20 // 1 MiB cap on body
|
||||
r.Body = http.MaxBytesReader(rw, r.Body, maxBody)
|
||||
defer func() { _ = r.Body.Close() }()
|
||||
|
||||
const max = 1 << 20 // 1 MiB cap on body
|
||||
buf := make([]byte, 0, 1024)
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, err := r.Body.Read(tmp)
|
||||
if n > 0 {
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if len(buf) > max {
|
||||
rw.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, http.ErrBodyReadAfterClose) || err != nil {
|
||||
break
|
||||
buf := webhookBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer putWebhookBuf(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
rw.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var u api.Update
|
||||
codec := w.Bot.Codec()
|
||||
if err := codec.Unmarshal(buf, &u); err != nil {
|
||||
if err := codec.Unmarshal(buf.Bytes(), &u); err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user