relay: introduce ConnectContext for better control over network latency

A websocket dial may hand for an unreasonably long time and a nostr client
has no control over this when trying to connect to a relay.

Go started introducing context in networking since 2014 -
see https://go.dev/blog/context - and by now many net functions have
XxxContext equivalent, such as DialContext.

Example usage of the change introduced by this commit:

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    r, err := nostr.RelayConnectContext(ctx, "ws://relay.example.org")

The code above makes RelayConnectContext last at most 3 sec, returning
an error if a connection cannot be established in the given time.
This helps whenever a tight control over connection latency is required,
such as distributed systems.

The change is backwards-compatible except the case where RelayPool.Add
sent an error over the returned channel without actually closing said
channel. I believe it was a bug.
This commit is contained in:
alex
2022-12-17 19:39:10 +01:00
committed by fiatjaf
parent ad71e083d8
commit c327f622f3
5 changed files with 138 additions and 33 deletions
+45 -31
View File
@@ -1,6 +1,7 @@
package nostr
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
@@ -58,44 +59,57 @@ func NewRelayPool() *RelayPool {
}
}
// Add adds a new relay to the pool, if policy is nil, it will be a simple
// read+write policy.
func (r *RelayPool) Add(url string, policy RelayPoolPolicy) chan error {
// Add calls AddContext with background context in a separate goroutine, sending
// any connection error over the returned channel.
//
// The returned channel is closed once the connection is successfully
// established or RelayConnectContext returned an error.
func (r *RelayPool) Add(url string, policy RelayPoolPolicy) <-chan error {
cherr := make(chan error)
go func() {
defer close(cherr)
if err := r.AddContext(context.Background(), url, policy); err != nil {
cherr <- err
}
}()
return cherr
}
// AddContext connects to a relay at a canonical version specified by the url
// and adds it to the pool. The returned error is non-nil only on connection
// errors, including an expired context before the connection is complete.
//
// Once successfully connected, AddContext returns and the context expiration
// has no effect: call r.Remove to close the connection and delete a relay from the pool.
func (r *RelayPool) AddContext(ctx context.Context, url string, policy RelayPoolPolicy) error {
relay, err := RelayConnectContext(ctx, url)
if err != nil {
return fmt.Errorf("failed to connect to %s: %w", url, err)
}
if policy == nil {
policy = SimplePolicy{Read: true, Write: true}
}
r.addConnected(relay, policy)
return nil
}
cherr := make(chan error)
func (r *RelayPool) addConnected(relay *Relay, policy RelayPoolPolicy) {
r.Policies.Store(relay.URL, policy)
r.Relays.Store(relay.URL, relay)
go func() {
relay, err := RelayConnect(url)
if err != nil {
cherr <- fmt.Errorf("failed to connect to %s: %w", url, err)
return
}
r.subscriptions.Range(func(id string, filters Filters) bool {
sub := relay.prepareSubscription(id)
sub.Sub(filters)
eventStream, _ := r.eventStreams.Load(id)
r.Policies.Store(relay.URL, policy)
r.Relays.Store(relay.URL, relay)
go func(sub *Subscription) {
for evt := range sub.Events {
eventStream <- EventMessage{Relay: relay.URL, Event: evt}
}
}(sub)
r.subscriptions.Range(func(id string, filters Filters) bool {
sub := relay.prepareSubscription(id)
sub.Sub(filters)
eventStream, _ := r.eventStreams.Load(id)
go func(sub *Subscription) {
for evt := range sub.Events {
eventStream <- EventMessage{Relay: relay.URL, Event: evt}
}
}(sub)
return true
})
cherr <- nil
close(cherr)
}()
return cherr
return true
})
}
// Remove removes a relay from the pool.