Monorepo for Tangled tangled.org

knotmirror: add knotBackoff and reachability test #1168

merged opened by boltless.me targeting master from sl/knotmirror

git-cli doesn't support http connection timeout, so we cannot set short 30s connection timeout on git fetch. We don't want to put operation timeout that short because intial git clone can take pretty long.

go-git does expose http client but only globally and is less efficient than cli. So as a hack, just fetch remote server to check if knot is available and is valid git remote server

Signed-off-by: Seongmin Lee git@boltless.me

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mh3qbj6yyk22
+84 -2
Diff #2
+84 -2
knotmirror/resyncer.go
··· 7 7 "fmt" 8 8 "log/slog" 9 9 "math/rand" 10 + "net/http" 11 + "net/url" 10 12 "strings" 11 13 "sync" 12 14 "time" ··· 32 34 manualResyncTimeout time.Duration 33 35 parallelism int 34 36 37 + knotBackoff map[string]time.Time 38 + knotBackoffMu sync.RWMutex 35 39 } 36 40 37 41 func NewResyncer(l *slog.Logger, db *sql.DB, gitm GitMirrorManager, cfg *config.Config) *Resyncer { ··· 45 49 repoFetchTimeout: cfg.GitRepoFetchTimeout, 46 50 manualResyncTimeout: 30 * time.Minute, 47 51 parallelism: cfg.ResyncParallelism, 52 + 53 + knotBackoff: make(map[string]time.Time), 48 54 } 49 55 } 50 56 ··· 204 210 return false, nil 205 211 } 206 212 207 - // TODO: check if Knot is on backoff list. If so, return (false, nil) 208 - // TODO: detect rate limit error (http.StatusTooManyRequests) to put Knot in backoff list 213 + r.knotBackoffMu.RLock() 214 + backoffUntil, inBackoff := r.knotBackoff[repo.KnotDomain] 215 + r.knotBackoffMu.RUnlock() 216 + if inBackoff && time.Now().Before(backoffUntil) { 217 + return false, nil 218 + } 219 + 220 + // HACK: check knot reachability with short timeout before running actual fetch. 221 + // This is crucial as git-cli doesn't support http connection timeout. 222 + // `http.lowSpeedTime` is only applied _after_ the connection. 223 + if err := r.checkKnotReachability(ctx, repo); err != nil { 224 + if isRateLimitError(err) { 225 + r.knotBackoffMu.Lock() 226 + r.knotBackoff[repo.KnotDomain] = time.Now().Add(10 * time.Second) 227 + r.knotBackoffMu.Unlock() 228 + return false, nil 229 + } 230 + // TODO: suspend repo on 404. KnotStream updates will change the repo state back online 231 + return false, fmt.Errorf("knot unreachable: %w", err) 232 + } 209 233 210 234 timeout := r.repoFetchTimeout 211 235 if repo.RetryAfter == -1 { ··· 230 254 return true, nil 231 255 } 232 256 257 + type knotStatusError struct { 258 + StatusCode int 259 + } 260 + 261 + func (ke *knotStatusError) Error() string { 262 + return fmt.Sprintf("request failed with status code (HTTP %d)", ke.StatusCode) 263 + } 264 + 265 + func isRateLimitError(err error) bool { 266 + var knotErr *knotStatusError 267 + if errors.As(err, &knotErr) { 268 + return knotErr.StatusCode == http.StatusTooManyRequests 269 + } 270 + return false 271 + } 272 + 273 + // checkKnotReachability checks if Knot is reachable and is valid git remote server 274 + func (r *Resyncer) checkKnotReachability(ctx context.Context, repo *models.Repo) error { 275 + repoUrl, err := makeRepoRemoteUrl(repo.KnotDomain, repo.DidSlashRepo(), true) 276 + if err != nil { 277 + return err 278 + } 279 + 280 + repoUrl += "/info/refs?service=git-upload-pack" 281 + 282 + client := http.Client{ 283 + Timeout: 30 * time.Second, 284 + } 285 + req, err := http.NewRequestWithContext(ctx, "GET", repoUrl, nil) 286 + if err != nil { 287 + return err 288 + } 289 + req.Header.Set("User-Agent", "git/2.x") 290 + req.Header.Set("Accept", "*/*") 291 + 292 + resp, err := client.Do(req) 293 + if err != nil { 294 + var uerr *url.Error 295 + if errors.As(err, &uerr) { 296 + return fmt.Errorf("request failed: %w", uerr.Unwrap()) 297 + } 298 + return fmt.Errorf("request failed: %w", err) 299 + } 300 + defer resp.Body.Close() 301 + 302 + if resp.StatusCode != http.StatusOK { 303 + return &knotStatusError{resp.StatusCode} 304 + } 305 + 306 + // check if target is git server 307 + ct := resp.Header.Get("Content-Type") 308 + if !strings.Contains(ct, "application/x-git-upload-pack-advertisement") { 309 + return fmt.Errorf("unexpected content-type: %s", ct) 310 + } 311 + 312 + return nil 313 + } 314 + 233 315 func (r *Resyncer) handleResyncFailure(ctx context.Context, repoAt syntax.ATURI, err error) error { 234 316 r.logger.Debug("handleResyncFailure", "at_uri", repoAt, "err", err) 235 317 var state models.RepoState

History

5 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
knotmirror: add knotBackoff and reachability test
2/3 failed, 1/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
knotmirror: add knotBackoff and reachability test
expand 0 comments
1 commit
expand
knotmirror: add knotBackoff and reachability test
expand 0 comments
1 commit
expand
knotmirror: add knotBackoff and reachability test
expand 0 comments
1 commit
expand
knotmirror: add knotBackoff and reachability test
expand 0 comments