Skip to content

Commit

Permalink
refactor(enginenetx): rename HTTPTransport to Network (#1293)
Browse files Browse the repository at this point in the history
The HTTPTransport model only offers a CloseIdleConnections callback,
which semantics is obviously that of closing idle connections
(unsurprisingly).

However, the struct I am slowly building inside the enginenetx package
soon will need to write back statistics to disk using a key-value store.

I don't think we should overload the CloseIdleConnections semantics to
do this job, since the resulting code would be quite surprising.

Therefore, I have decided to rename HTTPTransport to Network and make it
represent all the network abstractions required by the OONI engine.

In time, I will move extra functionality in there. For now, let us be
happy that we can easily define a Close method (currently empty) for
this type having the usual io.Closer semantics, i.e., that any resource
opened by the type itself is released when calling this method.

This diff does the following:

- it renames the files, the type, and the tests;

- it introduces a Close method that closes the Network's underlying
transport's idle connections;

- it adapts users of this code to use the new semantics;

- it introduces an model.KeyValueStore argument for the Network
constructor which we're going to use soon to persist statistics;

- updates `CONTRIBUTING.md` to say how we do internal testing when the
main tests body is external.

Part of ooni/probe#2531
  • Loading branch information
bassosimone authored Sep 22, 2023
1 parent 1e42525 commit e27eead
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 72 deletions.
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ Long-running tests should be skipped when running tests in short mode
using `go test -short`. We prefer internal testing to external
testing. We generally have a file called `foo_test.go` with tests
for every `foo.go` file. Sometimes we separate long running
integration tests in a `foo_integration_test.go` file.
integration tests in a `foo_integration_test.go` file. We also
sometimes have `foo_internal_test.go` when the main body of tests
for `foo`, i.e., `foo_test.go` uses external testing.

If there is a top-level DESIGN.md document, make sure such document is
kept in sync with code changes you have applied.
Expand Down
2 changes: 1 addition & 1 deletion internal/engine/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (e *experiment) OpenReportContext(ctx context.Context) error {
// use custom client to have proper byte accounting
httpClient := &http.Client{
Transport: bytecounter.WrapHTTPTransport(
e.session.httpDefaultTransport,
e.session.network.HTTPTransport(),
e.byteCounter,
),
}
Expand Down
11 changes: 7 additions & 4 deletions internal/engine/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type Session struct {
availableProbeServices []model.OOAPIService
availableTestHelpers map[string][]model.OOAPIService
byteCounter *bytecounter.Counter
httpDefaultTransport *enginenetx.HTTPTransport
network *enginenetx.Network
kvStore model.KeyValueStore
location *enginelocate.Results
logger model.Logger
Expand Down Expand Up @@ -213,8 +213,9 @@ func NewSession(ctx context.Context, config SessionConfig) (*Session, error) {
Logger: sess.logger,
ProxyURL: proxyURL,
}
sess.httpDefaultTransport = enginenetx.NewHTTPTransport(
sess.network = enginenetx.NewNetwork(
sess.byteCounter,
config.KVStore,
sess.logger,
proxyURL,
sess.resolver,
Expand Down Expand Up @@ -341,7 +342,9 @@ func (s *Session) Close() error {

// doClose implements Close. This function is called just once.
func (s *Session) doClose() {
s.httpDefaultTransport.CloseIdleConnections()
// make sure we close open connections and persist stats to the key-value store
s.network.Close()

s.resolver.CloseIdleConnections()
if s.tunnel != nil {
s.tunnel.Stop()
Expand All @@ -360,7 +363,7 @@ func (s *Session) GetTestHelpersByName(name string) ([]model.OOAPIService, bool)

// DefaultHTTPClient returns the session's default HTTP client.
func (s *Session) DefaultHTTPClient() model.HTTPClient {
return s.httpDefaultTransport.NewHTTPClient()
return s.network.NewHTTPClient()
}

// FetchTorTargets fetches tor targets from the API.
Expand Down
60 changes: 0 additions & 60 deletions internal/enginenetx/http.go

This file was deleted.

85 changes: 85 additions & 0 deletions internal/enginenetx/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package enginenetx

import (
"net/http"
"net/http/cookiejar"
"net/url"

"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
"golang.org/x/net/publicsuffix"
)

// Network is the network abstraction used by the OONI engine.
type Network struct {
txp model.HTTPTransport
}

// HTTPTransport returns the [model.HTTPTransport] that the engine should use.
func (n *Network) HTTPTransport() model.HTTPTransport {
return n.txp
}

// NewHTTPClient is a convenience function for building a [model.HTTPClient] using
// the underlying [model.HTTPTransport] and the correct cookies configuration.
func (n *Network) NewHTTPClient() *http.Client {
// Note: cookiejar.New cannot fail, so we're using runtimex.Try1 here
return &http.Client{
Transport: n.txp,
Jar: runtimex.Try1(cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})),
}
}

// Close ensures that we close idle connections and persist statistics.
func (n *Network) Close() error {
// TODO(bassosimone): do we want to introduce "once" semantics in this method?

// make sure we close the transport's idle connections
n.txp.CloseIdleConnections()

return nil
}

// NewNetwork creates a new [*Network] for the engine. This network MUST NOT be
// used for measuring because it implements engine-specific policies.
//
// You MUST call the Close method when done using the network. This method ensures
// that (i) we close idle connections and (ii) persist statistics.
//
// Arguments:
//
// - counter is the [*bytecounter.Counter] to use.
//
// - kvStore is a [model.KeyValueStore] for persisting stats;
//
// - logger is the [model.Logger] to use;
//
// - proxyURL is the OPTIONAL proxy URL;
//
// - resolver is the [model.Resolver] to use.
//
// The presence of the proxyURL will cause this function to possibly build a
// network with different behavior with respect to circumvention. If there is
// an upstream proxy we're going to trust it is doing circumvention for us.
func NewNetwork(
counter *bytecounter.Counter,
kvStore model.KeyValueStore,
logger model.Logger,
proxyURL *url.URL,
resolver model.Resolver,
) *Network {
dialer := netxlite.NewDialerWithResolver(logger, resolver)
handshaker := netxlite.NewTLSHandshakerStdlib(logger)
tlsDialer := netxlite.NewTLSDialer(dialer, handshaker)
txp := netxlite.NewHTTPTransportWithOptions(
logger, dialer, tlsDialer,
netxlite.HTTPTransportOptionDisableCompression(false),
netxlite.HTTPTransportOptionProxyURL(proxyURL), // nil implies "no proxy"
)
txp = bytecounter.WrapHTTPTransport(txp, counter)
return &Network{txp}
}
33 changes: 33 additions & 0 deletions internal/enginenetx/network_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package enginenetx

import (
"testing"

"github.com/ooni/probe-cli/v3/internal/mocks"
)

func TestNetworkUnit(t *testing.T) {
t.Run("HTTPTransport returns the correct transport", func(t *testing.T) {
expected := &mocks.HTTPTransport{}
netx := &Network{txp: expected}
if netx.HTTPTransport() != expected {
t.Fatal("not the transport we expected")
}
})

t.Run("Close calls the transport's CloseIdleConnections method", func(t *testing.T) {
var called bool
expected := &mocks.HTTPTransport{
MockCloseIdleConnections: func() {
called = true
},
}
netx := &Network{txp: expected}
if err := netx.Close(); err != nil {
t.Fatal(err)
}
if !called {
t.Fatal("did not call the transport's CloseIdleConnections")
}
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/enginenetx"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netemx"
Expand All @@ -19,14 +20,15 @@ import (
"github.com/ooni/probe-cli/v3/internal/testingx"
)

func TestHTTPTransportWAI(t *testing.T) {
func TestNetworkQA(t *testing.T) {
t.Run("is WAI when not using any proxy", func(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
defer env.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
txp := enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
netxlite.NewStdlibResolver(model.DiscardLogger),
Expand Down Expand Up @@ -61,8 +63,9 @@ func TestHTTPTransportWAI(t *testing.T) {
defer proxy.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
txp := enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
&url.URL{
Scheme: "socks5",
Expand Down Expand Up @@ -129,8 +132,9 @@ func TestHTTPTransportWAI(t *testing.T) {
defer proxy.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
txp := enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
&url.URL{
Scheme: "http",
Expand Down Expand Up @@ -199,8 +203,9 @@ func TestHTTPTransportWAI(t *testing.T) {
defer proxy.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
txp := enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
&url.URL{
Scheme: "https",
Expand Down Expand Up @@ -253,8 +258,9 @@ func TestHTTPTransportWAI(t *testing.T) {
})

t.Run("NewHTTPClient returns a client with a cookie jar", func(t *testing.T) {
txp := enginenetx.NewHTTPTransport(
txp := enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
netxlite.NewStdlibResolver(model.DiscardLogger),
Expand Down
2 changes: 2 additions & 0 deletions internal/kvstore/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
var ErrNoSuchKey = errors.New("no such key")

// Memory is an in-memory key-value store.
//
// The zero value is ready to use.
type Memory struct {
// m is the underlying map.
m map[string][]byte
Expand Down

0 comments on commit e27eead

Please sign in to comment.