auto-reconnecting jetstream proxy

use / as health check for upstream conn

+27 -2
+4
README.md
··· 7 7 NOTES: 8 8 - this should run as close to your infrastructure as possible. you then 9 9 would connect to `ws://localhost:6969/subscribe` 10 + - no cursor support. since there will be multiple jetstream upstreams that 11 + run at separate cursor timelines, it would be pretty hard to rewrite cursors 12 + in such a way that everything works. if your application relies on cursors, 13 + then it's probably best for your application to deal with multi-upstream support. 10 14 11 15 ``` 12 16 go build
+23 -2
main.go
··· 7 7 "os" 8 8 "strings" 9 9 "sync" 10 + "sync/atomic" 10 11 "time" 11 12 12 13 "github.com/gorilla/websocket" ··· 26 27 type Broadcaster struct { 27 28 listeners []chan []byte 28 29 mu sync.Mutex 30 + connected atomic.Bool 29 31 } 30 32 31 33 // Subscribe returns a new channel that will receive Jetstream events ··· 84 86 } 85 87 86 88 start := time.Now() 89 + // jetstream instances return the "Welcome to jetstream!" banner on / which 90 + // should be useful enough for latency 87 91 resp, err := client.Get(url) 88 92 if err != nil { 89 93 return 0, err ··· 99 103 }, 100 104 } 101 105 106 + // handleHealth returns 200 if connected to upstream 107 + func handleHealth(broadcaster *Broadcaster) http.HandlerFunc { 108 + return func(w http.ResponseWriter, r *http.Request) { 109 + if broadcaster.connected.Load() { 110 + w.WriteHeader(http.StatusOK) 111 + w.Write([]byte("Welcome to jetstream!")) 112 + } else { 113 + w.WriteHeader(http.StatusServiceUnavailable) 114 + w.Write([]byte("Not connected to upstream")) 115 + } 116 + } 117 + } 118 + 102 119 // handleSubscribe upgrades HTTP connection to websocket and streams events 103 120 func handleSubscribe(broadcaster *Broadcaster) http.HandlerFunc { 104 121 return func(w http.ResponseWriter, r *http.Request) { ··· 130 147 131 148 // connectToUpstream maintains a connection to the upstream websocket and broadcasts messages 132 149 func connectToUpstream(pool []string, broadcaster *Broadcaster) { 133 - backoff := time.Second 134 - maxBackoff := time.Minute 150 + backoff := 50 * time.Millisecond 151 + maxBackoff := 20 * time.Second 135 152 var currentUpstream string 136 153 137 154 for { ··· 157 174 conn, _, err := websocket.DefaultDialer.Dial(currentUpstream+"/subscribe", nil) 158 175 if err != nil { 159 176 slog.Error("Failed to connect to upstream", slog.String("url", currentUpstream), slog.Any("error", err)) 177 + broadcaster.connected.Store(false) 160 178 time.Sleep(backoff) 161 179 backoff *= 2 162 180 if backoff > maxBackoff { ··· 166 184 } 167 185 168 186 slog.Info("Connected to upstream", slog.String("url", currentUpstream)) 187 + broadcaster.connected.Store(true) 169 188 backoff = time.Second // Reset backoff on successful connection 170 189 171 190 // Read messages from upstream and broadcast them ··· 173 192 messageType, message, err := conn.ReadMessage() 174 193 if err != nil { 175 194 slog.Error("Error reading from upstream", slog.Any("error", err)) 195 + broadcaster.connected.Store(false) 176 196 conn.Close() 177 197 break 178 198 } ··· 271 291 go connectToUpstream(pool, broadcaster) 272 292 273 293 // Setup HTTP server 294 + http.HandleFunc("/", handleHealth(broadcaster)) 274 295 http.HandleFunc("/subscribe", handleSubscribe(broadcaster)) 275 296 276 297 slog.Info("Starting proxy server", slog.String("bind", bindAddr))