Add context cancellation support to database operations
- Add context-aware versions of all read operations in the database - Update handlers to use context from HTTP requests - Allows database queries to be cancelled when HTTP requests timeout - Prevents database operations from continuing after client disconnects
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -20,9 +19,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
risLiveURL = "https://ris-live.ripe.net/v1/stream/?format=json"
|
||||
risLiveURL = "https://ris-live.ripe.net/v1/stream/?format=json&" +
|
||||
"client=https%3A%2F%2Fgit.eeqj.de%2Fsneak%2Froutewatch"
|
||||
metricsWindowSize = 60 // seconds for rolling average
|
||||
metricsUpdateRate = time.Second
|
||||
minBackoffDelay = 5 * time.Second
|
||||
maxBackoffDelay = 320 * time.Second
|
||||
metricsLogInterval = 10 * time.Second
|
||||
bytesPerKB = 1024
|
||||
bytesPerMB = 1024 * 1024
|
||||
@@ -135,9 +137,7 @@ func (s *Streamer) Start() error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := s.stream(ctx); err != nil {
|
||||
s.logger.Error("Streaming error", "error", err)
|
||||
}
|
||||
s.streamWithReconnect(ctx)
|
||||
s.mu.Lock()
|
||||
s.running = false
|
||||
s.mu.Unlock()
|
||||
@@ -324,6 +324,72 @@ func (s *Streamer) updateMetrics(messageBytes int) {
|
||||
s.metrics.RecordMessage(int64(messageBytes))
|
||||
}
|
||||
|
||||
// streamWithReconnect handles streaming with automatic reconnection and exponential backoff
|
||||
func (s *Streamer) streamWithReconnect(ctx context.Context) {
|
||||
backoffDelay := minBackoffDelay
|
||||
consecutiveFailures := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.logger.Info("Stream context cancelled, stopping reconnection attempts")
|
||||
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Attempt to stream
|
||||
startTime := time.Now()
|
||||
err := s.stream(ctx)
|
||||
streamDuration := time.Since(startTime)
|
||||
|
||||
if err == nil {
|
||||
// Clean exit (context cancelled)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the error
|
||||
s.logger.Error("Stream disconnected",
|
||||
"error", err,
|
||||
"consecutive_failures", consecutiveFailures+1,
|
||||
"stream_duration", streamDuration)
|
||||
s.metrics.SetConnected(false)
|
||||
|
||||
// Check if context is cancelled
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we streamed for more than 30 seconds, reset the backoff
|
||||
// This indicates we had a successful connection that received data
|
||||
if streamDuration > 30*time.Second {
|
||||
s.logger.Info("Resetting backoff delay due to successful connection",
|
||||
"stream_duration", streamDuration)
|
||||
backoffDelay = minBackoffDelay
|
||||
consecutiveFailures = 0
|
||||
} else {
|
||||
// Increment consecutive failures
|
||||
consecutiveFailures++
|
||||
}
|
||||
|
||||
// Wait with exponential backoff
|
||||
s.logger.Info("Waiting before reconnection attempt",
|
||||
"delay_seconds", backoffDelay.Seconds(),
|
||||
"consecutive_failures", consecutiveFailures)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(backoffDelay):
|
||||
// Double the backoff delay for next time, up to max
|
||||
backoffDelay *= 2
|
||||
if backoffDelay > maxBackoffDelay {
|
||||
backoffDelay = maxBackoffDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamer) stream(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", risLiveURL, nil)
|
||||
if err != nil {
|
||||
@@ -394,10 +460,13 @@ func (s *Streamer) stream(ctx context.Context) error {
|
||||
// Parse the message first
|
||||
var wrapper ristypes.RISLiveMessage
|
||||
if err := json.Unmarshal(line, &wrapper); err != nil {
|
||||
// Output the raw line and panic on parse failure
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse JSON: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "Raw line: %s\n", string(line))
|
||||
panic(fmt.Sprintf("JSON parse error: %v", err))
|
||||
// Log the error and return to trigger reconnection
|
||||
s.logger.Error("Failed to parse JSON",
|
||||
"error", err,
|
||||
"line", string(line),
|
||||
"line_length", len(line))
|
||||
|
||||
return fmt.Errorf("JSON parse error: %w", err)
|
||||
}
|
||||
|
||||
// Check if it's a ris_message wrapper
|
||||
@@ -447,18 +516,11 @@ func (s *Streamer) stream(ctx context.Context) error {
|
||||
// Peer state changes - silently ignore
|
||||
continue
|
||||
default:
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"UNKNOWN MESSAGE TYPE: %s\nRAW MESSAGE: %s\n",
|
||||
msg.Type,
|
||||
string(line),
|
||||
)
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"Unknown RIS message type: %s",
|
||||
msg.Type,
|
||||
),
|
||||
s.logger.Error("Unknown message type",
|
||||
"type", msg.Type,
|
||||
"line", string(line),
|
||||
)
|
||||
panic(fmt.Sprintf("Unknown RIS message type: %s", msg.Type))
|
||||
}
|
||||
|
||||
// Dispatch to interested handlers
|
||||
|
||||
Reference in New Issue
Block a user