Skip to content

Commit ca1618d

Browse files
committed
fix(telegram): 3 reliability fixes from perf review
1. Health endpoint always returned 503 — defer SetReady() never ran during bot lifetime. Changed to immediate call after server start. 2. Data race on HealthServer.ready — read by HTTP handlers, written by main goroutine without synchronization. Changed to atomic.Bool. 3. Integer overflow in poller.backoffDuration — 1<<errors overflowed when consecutiiveErrors > 30 (extended API outage). Clamped to 30.
1 parent 4fd195d commit ca1618d

4 files changed

Lines changed: 13 additions & 7 deletions

File tree

cmd/odek/telegram.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,8 @@ func telegramCmd(args []string) error {
482482
fmt.Fprintf(os.Stderr, "odek telegram: health server: %v\n", err)
483483
}
484484
}()
485-
// Mark ready once polling begins.
486-
defer healthSrv.SetReady()
485+
// Mark ready — the server goroutine is already listening.
486+
healthSrv.SetReady()
487487
}
488488

489489
// 15. Handle SIGINT/SIGTERM/SIGHUP.

internal/telegram/health.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"net/http"
99
"os"
10+
"sync/atomic"
1011
"time"
1112
)
1213

@@ -15,7 +16,7 @@ import (
1516
type HealthServer struct {
1617
addr string
1718
startTime time.Time
18-
ready bool
19+
ready atomic.Bool
1920
log Logger
2021
}
2122

@@ -39,8 +40,9 @@ func (hs *HealthServer) SetLogger(l Logger) {
3940
}
4041

4142
// SetReady marks the health server as ready (polling has started).
43+
// Thread-safe: safe to call from any goroutine.
4244
func (hs *HealthServer) SetReady() {
43-
hs.ready = true
45+
hs.ready.Store(true)
4446
}
4547

4648
// ServeHTTP implements http.Handler.
@@ -52,7 +54,7 @@ func (hs *HealthServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5254

5355
w.Header().Set("Content-Type", "application/json")
5456

55-
if !hs.ready {
57+
if !hs.ready.Load() {
5658
w.WriteHeader(http.StatusServiceUnavailable)
5759
json.NewEncoder(w).Encode(map[string]any{
5860
"status": "starting",

internal/telegram/health_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
func TestHealthServer_Returns200(t *testing.T) {
1212
hs := NewHealthServer("127.0.0.1:0")
13-
hs.ready = true
13+
hs.ready.Store(true)
1414

1515
ts := &http.Server{Addr: hs.addr, Handler: hs}
1616
ln, err := net.Listen("tcp", "127.0.0.1:0")

internal/telegram/poller.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ func (p *Poller) SetLogger(l Logger) {
3939
// backoffDuration returns the sleep duration after consecutive poll errors.
4040
// Formula: interval * 2^errors, capped at 60 * interval.
4141
// Returns 0 for 0 errors (no backoff needed).
42+
// errors is clamped to 30 to prevent integer overflow in 1<<errors.
4243
func (p *Poller) backoffDuration(errors int) time.Duration {
4344
if errors <= 0 {
4445
return 0
4546
}
46-
shift := 1 << errors // 2^errors
47+
if errors > 30 {
48+
errors = 30
49+
}
50+
shift := 1 << errors // 2^errors, safe after clamp
4751
d := p.Interval * time.Duration(shift)
4852
max := 60 * p.Interval
4953
if d > max {

0 commit comments

Comments
 (0)