Releases: indexexchange/haproxy-spoe-go
Release list
v1.0.10 — Zero-allocation frame encode: pooled write buffers, Marshal appends in place
Write-path allocation elimination
Production profiling showed the frame write path accounted for ~46% of all allocated
bytes in a high-throughput SPOA under load: Action.Marshal grew a fresh payload
slice per ACK (27.2%), and Frame.Encode built every response in a throwaway
bytes.Buffer which writeFrame then copied into a second one (18.4%). Combined
with the KV.Reset fix in v1.0.9, this release removes the SPOE library from the
allocation profile almost entirely.
Changes
Frame.Encodewrites from a pooled buffer — the full wire frame (length
prefix included) is built in awriteBufretained on the pooled frame across
reuse cycles (mirror of the v1.0.8 read-buffer reuse), then written to the
connection in a singleWritecall. The single write is what keeps
concurrent ACK writers from interleaving frames on the wire — previously
guaranteed implicitly by the throwaway buffer, now explicit and documented.Action.Marshalappends into the caller's buffer instead of growing a nil
slice per action; returns the buffer even on error so pooled callers never
lose it.typeddata.Encodeappends in place with stack-allocated varint scratch
instead ofmake([]byte, 8)per value.- All per-frame
bytes.Buffers removed —writeFrame,sendAgentHello,
sendAgentDisconnect, and the test client now encode straight to the
connection.
Benchmarks
BenchmarkFrame_EncodeAck (pooled frame, 4 SetVar actions, go1.26):
| ns/op | B/op | allocs/op | |
|---|---|---|---|
| before | 272.8 | 488 | 11 |
| after | 69.9 | 0 | 0 |
Production additionally saves writeFrame's second buffer copy, not captured in
the benchmark.
Validation
Load-tested at 100k QPS before merge: Action.Marshal, KV.UnmarshalNB, and the
encode-buffer bytes.growSlice sites — ~72% of allocated bytes on the previous
release — no longer appear in the allocation profile. GC+malloc CPU share dropped
from ~10% to ~2%; GC cycle rate fell 6×.
v1.0.9 — KV.Reset keeps pooled slice capacity; Go 1.26
Performance
-
KV.Reset()now keeps slice capacity across pool reuses. Previously it rebuilt
the backing slice withmake([]Item, 0)on every pool release, forcing each pooled
KV to re-grow from zero per message — ~24% of all allocated bytes in production
profiles of a high-throughput agent (300k QPS). Reset now clears the items (so
pooled KVs don't pin the previous request's values) and truncates in place. -
New
payload/kvtests guard the behavior: capacity retention, value clearing, and
pool round-trip.
Benchmark
BenchmarkAddAfterReset — Reset + 16 Adds, the per-message pooled-KV pattern
(Intel Ultra 7 265HX, go1.26):
| ns/op | B/op | allocs/op | |
|---|---|---|---|
before (make([]Item, 0)) |
243.3 | 992 | 5 |
after (clear + kv.m[:0]) |
27.4 | 0 | 0 |
Toolchain
- Go directive bumped
1.19→1.26(matches consumers; enables theclearbuiltin).
CI updated to Go 1.26. - Fixed a pre-existing vet error in
worker/worker_test.go(t.Fatalfrom a non-test
goroutine) surfaced by the newer toolchain.
Note: the Go directive bump means consumers must build with Go 1.26+.
v1.0.8 — First Index Exchange release: high-throughput tuning & HAProxy 3.x support
First release under github.com/indexexchange/haproxy-spoe-go, forked from
negasus/haproxy-spoe-go v1.0.7 and tuned for high-throughput, high-concurrency
workloads on many-CPU machines.
Changes since upstream v1.0.7
- Module renamed to
github.com/indexexchange/haproxy-spoe-go(importable from Go projects) - Reuse read buffer across pooled frames to reduce allocation churn (negasus#28)
- Treat connection reset as normal close — HAProxy 3.x
mux_spopcompatibility (negasus#27)
Install
go get github.com/indexexchange/haproxy-spoe-go@v1.0.8