summaryrefslogtreecommitdiff
path: root/network/rateLimitingTransport.go
blob: 461a468da53c73235bd3c9f22f42cab5443f2767 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Copyright (C) 2019-2024 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand.  If not, see <https://www.gnu.org/licenses/>.

package network

import (
	"errors"
	"net/http"
	"time"

	"github.com/algorand/go-algorand/util"
)

// rateLimitingTransport is the transport for execute a single HTTP transaction, obtaining the Response for a given Request.
type rateLimitingTransport struct {
	phonebook       Phonebook
	innerTransport  *http.Transport
	queueingTimeout time.Duration
}

// ErrConnectionQueueingTimeout indicates that we've exceeded the time allocated for
// queueing the current request before the request attempt could be made.
var ErrConnectionQueueingTimeout = errors.New("rateLimitingTransport: queueing timeout")

// makeRateLimitingTransport creates a rate limiting http transport that would limit the requests rate
// according to the entries in the phonebook.
func makeRateLimitingTransport(phonebook Phonebook, queueingTimeout time.Duration, dialer *Dialer, maxIdleConnsPerHost int) rateLimitingTransport {
	defaultTransport := http.DefaultTransport.(*http.Transport)
	return rateLimitingTransport{
		phonebook: phonebook,
		innerTransport: &http.Transport{
			Proxy:                 defaultTransport.Proxy,
			DialContext:           dialer.innerDialContext,
			MaxIdleConns:          defaultTransport.MaxIdleConns,
			IdleConnTimeout:       defaultTransport.IdleConnTimeout,
			TLSHandshakeTimeout:   defaultTransport.TLSHandshakeTimeout,
			ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
			MaxIdleConnsPerHost:   maxIdleConnsPerHost,
		},
		queueingTimeout: queueingTimeout,
	}
}

// RoundTrip connects to the address on the named network using the provided context.
// It waits if needed not to exceed connectionsRateLimitingCount.
func (r *rateLimitingTransport) RoundTrip(req *http.Request) (res *http.Response, err error) {
	var waitTime time.Duration
	var provisionalTime time.Time
	queueingDeadline := time.Now().Add(r.queueingTimeout)
	for {
		_, waitTime, provisionalTime = r.phonebook.GetConnectionWaitTime(req.Host)
		if waitTime == 0 {
			break // break out of the loop and proceed to the connection
		}
		waitDeadline := time.Now().Add(waitTime)
		if waitDeadline.Before(queueingDeadline) {
			util.NanoSleep(waitTime)
			continue
		}
		return nil, ErrConnectionQueueingTimeout
	}
	res, err = r.innerTransport.RoundTrip(req)
	r.phonebook.UpdateConnectionTime(req.Host, provisionalTime)
	return
}