Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

eventconsumer: rework retry mechanism

the previous retry mechanism had a slight flaw: successful connections
did not reset the exponent on the retry interval. this results in
constantly growing retry intervals:

attempt #1 - wait 5s
attempt #2 - wait 10s
attempt #3 - success!
.
.
.
disconnect
attempt #4 - wait 20s

what we want to see however, is a pattern like so:

attempt #1 - wait 5s
attempt #2 - wait 10s
attempt #3 - success!
.
.
.
disconnect
attempt #1 - wait 5s

this is solved by slapping the retry logic around DialConnection, which
is a more atomic point of connection attempt. retry logic is also
offloaded to the github.com/avast-go/retry package

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by

Tangled 037bdc4c dea92276

+35 -25
+31 -24
eventconsumer/consumer.go
··· 12 12 "tangled.sh/tangled.sh/core/eventconsumer/cursor" 13 13 "tangled.sh/tangled.sh/core/log" 14 14 15 + "github.com/avast/retry-go/v4" 15 16 "github.com/gorilla/websocket" 16 17 ) 17 18 ··· 171 170 172 171 func (c *Consumer) startConnectionLoop(ctx context.Context, source Source) { 173 172 defer c.wg.Done() 174 - retryInterval := c.cfg.RetryInterval 173 + 175 174 for { 176 175 select { 177 176 case <-ctx.Done(): ··· 179 178 default: 180 179 err := c.runConnection(ctx, source) 181 180 if err != nil { 182 - c.logger.Error("connection failed", "source", source, "err", err) 183 - } 184 - 185 - // apply jitter 186 - jitter := time.Duration(c.randSource.Int63n(int64(retryInterval) / 5)) 187 - delay := retryInterval + jitter 188 - 189 - if retryInterval < c.cfg.MaxRetryInterval { 190 - retryInterval *= 2 191 - if retryInterval > c.cfg.MaxRetryInterval { 192 - retryInterval = c.cfg.MaxRetryInterval 193 - } 194 - } 195 - c.logger.Info("retrying connection", "source", source, "delay", delay) 196 - select { 197 - case <-time.After(delay): 198 - case <-ctx.Done(): 199 - return 181 + c.logger.Error("failed to run connection", "err", err) 200 182 } 201 183 } 202 184 } 203 185 } 204 186 205 187 func (c *Consumer) runConnection(ctx context.Context, source Source) error { 206 - connCtx, cancel := context.WithTimeout(ctx, c.cfg.ConnectionTimeout) 207 - defer cancel() 208 - 209 188 cursor := c.cfg.CursorStore.Get(source.Key()) 210 189 211 190 u, err := source.Url(cursor, c.cfg.Dev) ··· 194 213 } 195 214 196 215 c.logger.Info("connecting", "url", u.String()) 197 - conn, _, err := c.dialer.DialContext(connCtx, u.String(), nil) 216 + 217 + retryOpts := []retry.Option{ 218 + retry.Attempts(0), // infinite attempts 219 + retry.DelayType(retry.BackOffDelay), 220 + retry.Delay(c.cfg.RetryInterval), 221 + retry.MaxDelay(c.cfg.MaxRetryInterval), 222 + retry.MaxJitter(c.cfg.RetryInterval / 5), 223 + retry.OnRetry(func(n uint, err error) { 224 + c.logger.Info("retrying connection", 225 + "source", source, 226 + "url", u.String(), 227 + "attempt", n+1, 228 + "err", err, 229 + ) 230 + }), 231 + retry.Context(ctx), 232 + } 233 + 234 + var conn *websocket.Conn 235 + 236 + err = retry.Do(func() error { 237 + connCtx, cancel := context.WithTimeout(ctx, c.cfg.ConnectionTimeout) 238 + defer cancel() 239 + conn, _, err = c.dialer.DialContext(connCtx, u.String(), nil) 240 + return err 241 + }, retryOpts...) 198 242 if err != nil { 199 243 return err 200 244 } 201 - defer conn.Close() 245 + 202 246 c.connMap.Store(source, conn) 247 + defer conn.Close() 203 248 defer c.connMap.Delete(source) 204 249 205 250 c.logger.Info("connected", "source", source)
+1 -1
flake.nix
··· 61 61 inherit (gitignore.lib) gitignoreSource; 62 62 in { 63 63 overlays.default = final: prev: let 64 - goModHash = "sha256-2RUwj16RNaZ/gCOcd7b3LRCHiROCRj9HuzbBdLdgWGo="; 64 + goModHash = "sha256-SLi+nALwCd/Lzn3aljwPqCo2UaM9hl/4OAjcHQLt2Bk="; 65 65 appviewDeps = { 66 66 inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src goModHash gitignoreSource; 67 67 };
+1
go.mod
··· 49 49 github.com/Microsoft/go-winio v0.6.2 // indirect 50 50 github.com/ProtonMail/go-crypto v1.2.0 // indirect 51 51 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 52 + github.com/avast/retry-go/v4 v4.6.1 // indirect 52 53 github.com/aymerick/douceur v0.2.0 // indirect 53 54 github.com/beorn7/perks v1.0.1 // indirect 54 55 github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
+2
go.sum
··· 17 17 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 18 18 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 19 19 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 20 + github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= 21 + github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 20 22 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 21 23 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 22 24 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=