-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTTP retrieval proposal #747
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @hsanjuan, would be extremely nice if we can pull it off with such small set of changes.
Once we have HTTP basics like user-agent, status code metrics, 503/429/Retry-After (details inline), this is worth testing on Rainbow staging (do A/B test with bitswap-only box and bitswap+http).
ps. Whatever we do, HTTP should be opt-in, with a big EXPERIMENTAL warning.
279d563
to
7e1160b
Compare
5a73303
to
c6a1b06
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-review.
@@ -191,7 +191,7 @@ func New(parent context.Context, network bsnet.BitSwapNetwork, providerFinder Pr | |||
|
|||
sim := bssim.New() | |||
bpm := bsbpm.New() | |||
pm := bspm.New(ctx, peerQueueFactory, network.Self()) | |||
pm := bspm.New(ctx, peerQueueFactory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out, peer ID is not used in the peermanager.
package bsnet | ||
|
||
import "github.com/ipfs/boxo/bitswap/network/bsnet/internal" | ||
|
||
var ( | ||
// ProtocolBitswapNoVers is equivalent to the legacy bitswap protocol | ||
ProtocolBitswapNoVers = internal.ProtocolBitswapNoVers | ||
// ProtocolBitswapOneZero is the prefix for the legacy bitswap protocol | ||
ProtocolBitswapOneZero = internal.ProtocolBitswapOneZero | ||
// ProtocolBitswapOneOne is the prefix for version 1.1.0 | ||
ProtocolBitswapOneOne = internal.ProtocolBitswapOneOne | ||
// ProtocolBitswap is the current version of the bitswap protocol: 1.2.0 | ||
ProtocolBitswap = internal.ProtocolBitswap | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved from network
, which now is more an "exchange" network with interfaces and common utils.
@@ -1,4 +1,4 @@ | |||
package network | |||
package bsnet |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes to this file and others in the module are cosmetic, renames...
@@ -20,7 +23,7 @@ const ( | |||
stateUnresponsive | |||
) | |||
|
|||
type connectEventManager struct { | |||
type ConnectEventManager struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ConnEventManager has been extracted from bsnet
, it is now re-used in httpnet
, therefore exposed. Changes are otherwise cosmetic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, Bitswap is the name of the protocol (and client behaviour) and it can use either HTTP or libp2p to communicate with remote peers. But then HTTP servers don't exactly follow Bitswap spec since they don't comply with CANCEL
messages (see comment).
My suggestion would be to name the folders network/libp2p
and network/http
.
case entry.WantType == pb.Message_Wantlist_Block: | ||
method = "GET" | ||
case entry.WantType == pb.Message_Wantlist_Have: | ||
method = "HEAD" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WANT_HAVE
requests are part of Bitswap 1.2 (PR), so I think it is worth it to support HEAD requests. It is the newer bitswap that introduces WANT_HAVE
, and HAVE
messages.
If some endpoints consistently throw 500s on HEAD, we can stop sending these and focus on GET. We should look into the behaviour of a client running bitswap 1.2 and server running bitswap 1.0 or 1.1 and try to replicate.
WANT_HAVE
represent a large share of all bitswap messages (see bitswap study), and play a key role in bitswap content routing (the bitswap spamming...).
case entry.Cancel: | ||
// log.Debugf("received cancel entry for %s: %s", u.url, entry.Cid) | ||
sender.ht.requestTracker.cancelRequest(entry.Cid) | ||
return nil // cont with next block |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing comment here to enable discussion:
CANCEL
messages are part of Bitswap spec since version 1.0.0, and the client's behaviour depends on it. E.g it will not ask twice the same peer about the same CID, since it expects that the peer would remember the interest.
The spec says that Bitswap clients SHOULD send CANCEL
messages, so I guess it is okay if it doesn't send CANCEL
to HTTP server (anyway this makes no sense). The specs doesn't mention that peers should record the wantlist of connected nodes. However, since there is only 1 bitswap server implementation (deployed, to my knowledge), it was easy to assume that all bitswap peers would record connected nodes wantlists, and to rely on that when designing the client.
Since this PR allows the Bitswap client to talk Bitswap with HTTP servers, it means that not all Bitswap servers will be able to record connected nodes wantlist anymore.
Maybe the Bitswap client behaviour needs to be adjusted to reflect that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjusted how though? We cannot get a callback from the http server when it obtains blocks from somewhere else that we wanted.
I think the client code compensates for this by essentially broadcast-retrying. It may be slower though in some scenario. That said, we are still running bitswap on the side and in parallel, so bitswap-servers contacted for other reasons will still have this wantlist visibility i guess?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the client can send WANT_HAVE
more aggressively to HTTP nodes compared with libp2p nodes?
Otherwise we don't compensate, and we document clearly where required that bitswap+libp2p will have a better performance for content routing (not necessarily content retrieval) compared with bitswap+http.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to sync about this, as it affects the same-peer-id vs. different-peer-id for bitswap/http endpoints. Perhaps we need to notify the bitswap server about wantlists that are requested over http just so it knows about them. I'm not familiar with the bitswap server code though, so not sure what happens there.
} | ||
|
||
// New returns a BitSwapNetwork supported by underlying IPFS host. | ||
func New(pstore peerstore.Peerstore, bitswap BitSwapNetwork, http BitSwapNetwork) BitSwapNetwork { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add flag to specify whether http or libp2p should be preferred by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to benchmark, but I can imagine bitswap+libp2p being more efficient that bitswap+http for large wantlists, since each entry is a distinct http request, but the full wantlist fits in a single libp2p message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently wantlists are sent in a single stream, but we wanted to change it into one stream per request (in pure bitswap). Realistically very difficult to benchmark. A gateway with CDN caching will beat bitswap for sure. I don't know about a Kubo gateway, probably depends a lot on the shape of requests (as you say, not the same to send a wantlist of 1 than to send a wantlist of 200).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think bitswap+http will beat bitswap+libp2p at pure content retrieval (e.g you already know the address of the provider thank to DHT or IPNI), but for content routing+retrieval (broadcasting WANT_HAVE
) bitswap+libp2p is expected to find+fetch the content faster than bitswap+http.
Maybe http+bitswap could prioritize sending WANT_BLOCK
over WANT_HAVE
, since the client sends WANT_BLOCK
only if it thinks the remote has a good probability of having the block, whereas it literally broadcast the WANT_HAVE
.
case entry.WantType == pb.Message_Wantlist_Block: | ||
method = "GET" | ||
case entry.WantType == pb.Message_Wantlist_Have: | ||
method = "HEAD" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since each request targets a specific CID, and requests are sent sequentially, it means that the http server will be spammed if the wantlist contains 100's of entries (each corresponding to a WANT_HAVE
message). The number of WANT_BLOCK
is expected to be much smaller.
Maybe due to the sequential requests nature of the message sender, it is best not to send WANT_HAVE
after all, even if it means that the bitswap+http doesn't do what it SHOULD from the spec.
case entry.Cancel: | ||
// log.Debugf("received cancel entry for %s: %s", u.url, entry.Cid) | ||
sender.ht.requestTracker.cancelRequest(entry.Cid) | ||
return nil // cont with next block |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the client can send WANT_HAVE
more aggressively to HTTP nodes compared with libp2p nodes?
Otherwise we don't compensate, and we document clearly where required that bitswap+libp2p will have a better performance for content routing (not necessarily content retrieval) compared with bitswap+http.
} | ||
|
||
// New returns a BitSwapNetwork supported by underlying IPFS host. | ||
func New(pstore peerstore.Peerstore, bitswap BitSwapNetwork, http BitSwapNetwork) BitSwapNetwork { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think bitswap+http will beat bitswap+libp2p at pure content retrieval (e.g you already know the address of the provider thank to DHT or IPNI), but for content routing+retrieval (broadcasting WANT_HAVE
) bitswap+libp2p is expected to find+fetch the content faster than bitswap+http.
Maybe http+bitswap could prioritize sending WANT_BLOCK
over WANT_HAVE
, since the client sends WANT_BLOCK
only if it thinks the remote has a good probability of having the block, whereas it literally broadcast the WANT_HAVE
.
Note about the two scenarios regarding peerIDs:
Resolving A issues imply:
Resolving B issues imply:
|
This and subsequent commits introduce an httpnet module at what is known as the "bitswap network layer". The bitswap network layer connects bitswap-peers, sends bitswap messages and receives responses. Bitswap messages are basically a wantlist, a list of CIDs that should be sent if available. httpnet does the same, except instead of sending the bitswap message over bitswap, it triggers http requests for the requested blocks. httpnet is a drop-in addon so that we can request blocks over http, and not only via bitswap. As httpnet is a network, it benefits from all existing wantlist management logic. Any http/2 endpoint should benefit from streamlined requests on a single http connection. A router-network ensures that messages are correctly handled by bitswap or by http requests depending on what the peers are advertising. HTTP requests are given priority in the presence of both. Here are some of the httpnet features: * Peers are marked as Connected when they are able to handle http requets. * Peers are marked as Disconnected when http requests fail repeatedly (MaxRetries). * Server errors trigger backoffs preventing more requests to happen to the same url for a period (Retry-After header or configuration value) * We support several urls per peer, meaning a peer can provide alternative http endpoints which are tried based on number of failures or existing cooldowns. * We translate HAVE requests to HTTP-HEAD requests and BLOCK requests to HTTP-GETs * We support cancellations: ongoing or soon to happen requests for a CID can be cancelled using a "cancel" entry in the wantlist. * We record latency information for peers by pinging regularly. * We discriminate between different errors so that we know whether to move to the next block in a wantlist, or to retry with a different url, or to completely abort. * Options to configure user-agent, max retries etc. are supported.
Do not consider successful a connection attempt that returns 500. Avoid goroutine leak when using SendMsg() many times with the same MessageSender.
…ailable Error counts for these statuses are tracked separately. Each 3 errors, the serverError count increases.
This is a proposal to add HTTP retrieval to Boxo. The current state is highly WIP, but I successfully retrieved something over HTTP, so posting to initiate a discussion over the approach and if we want to pursue it until the end.
Approach
The high-level idea is that most of what lives in
bitswap/client
is actually an "exchange" implementation, with the only real "Bitswap" thing being thatbitswap/network
sends HAS/GET requests over bitswap-protocol streams. As such, we should be able to complementbitswap/network
with an HTTP-retrieval implementation which, instead of fetching things over the bitswap protocol, calls HTTP endpoints as indicated by the provider's/http
addresses entries.Note that conceptually at least, this is not adding HTTP retrieval into bitswap, but promoting most of the bitswap code to be a reference "Exchange" implementation, which is re-usable for different retrieval protocols (bitswap, http...). That is, we would be talking of an "exchange network" component and not a "bitswap network" component. Renames to this extent are still missing.
Implementation
In order to introduce an http-retrieval "exchange network" we need to:
/http
provider.To this end:
/http
addresses in the peerstore of the given peer./http
endpoints when handling a WANT.In my tests plugging it to Kubo, the http-network can be used to retrieve content from a gateway over http. 🥳
The main advantange to this approach is that it is relatively clean to incorporate to the codebase, and keeps most of the code untouched, without having to duplicate any of the complex areas.
Challenges
Bitswap places a lot of importance on managing connectivity events to peers. We avoid requesting things from peers that have not signaled connectivity, we clean peers that have disconnected and re-queue things for peers that disconnect. Thus it seems we must support http-connectivity events. When a libp2p peer connects for bitswap, we know that the connection is setup, handshake has been performed and protocol negotiation has happened. For HTTP these things may not exist so we need to define what means "Connected" (i.e. in the case of https it would mean we have completed SSL handshakes).
Apart from that, the question is what are the elements in the current
bitswap/client
stack that do not apply to HTTP (peerqueues, messagequeues, broadcast, wantsending, prioritization etc.)... and why not? What if a peer disconnects from bitswap but not from http or vice-versa? What if Latency is much worse for bitswap than for http? Perhaps this is all logic for the network-router to know how to choose which network to use to send messages.Otherwise perhaps it is not possible to have a satisfactory implementation this way and we need to start thinking what to copy-paste into a separate "http-exchange" (at least the client part).
Related: #608