diff --git a/components/guns/http/base.go b/components/guns/http/base.go index c7d2fb348..fa4faeb3a 100644 --- a/components/guns/http/base.go +++ b/components/guns/http/base.go @@ -12,9 +12,11 @@ import ( "net/url" "github.com/pkg/errors" + "go.uber.org/zap" + "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/aggregator/netsample" - "go.uber.org/zap" + "github.com/yandex/pandora/core/warmup" ) const ( @@ -48,17 +50,17 @@ type HTTPTraceConfig struct { func DefaultBaseGunConfig() BaseGunConfig { return BaseGunConfig{ - AutoTagConfig{ + AutoTag: AutoTagConfig{ Enabled: false, URIElements: 2, NoTagOnly: true, }, - AnswLogConfig{ + AnswLog: AnswLogConfig{ Enabled: false, Path: "answ.log", Filter: "error", }, - HTTPTraceConfig{ + HTTPTrace: HTTPTraceConfig{ DumpEnabled: false, TraceEnabled: false, }, @@ -68,17 +70,26 @@ func DefaultBaseGunConfig() BaseGunConfig { type BaseGun struct { DebugLog bool // Automaticaly set in Bind if Log accepts debug messages. Config BaseGunConfig - Do func(r *http.Request) (*http.Response, error) // Required. - Connect func(ctx context.Context) error // Optional hook. - OnClose func() error // Optional. Called on Close(). - Aggregator netsample.Aggregator // Lazy set via BindResultTo. + Connect func(ctx context.Context) error // Optional hook. + OnClose func() error // Optional. Called on Close(). + Aggregator netsample.Aggregator // Lazy set via BindResultTo. AnswLog *zap.Logger + + scheme string + hostname string + targetResolved string + client Client + core.GunDeps } var _ Gun = (*BaseGun)(nil) var _ io.Closer = (*BaseGun)(nil) +func (b *BaseGun) WarmUp(_ *warmup.Options) (any, error) { + return nil, nil +} + func (b *BaseGun) Bind(aggregator netsample.Aggregator, deps core.GunDeps) error { log := deps.Log if ent := log.Check(zap.DebugLevel, "Gun bind"); ent != nil { @@ -157,7 +168,7 @@ func (b *BaseGun) Shoot(ammo Ammo) { } } var res *http.Response - res, err = b.Do(req) + res, err = b.client.Do(req) if b.Config.HTTPTrace.TraceEnabled && timings != nil { sample.SetReceiveTime(timings.GetReceiveTime()) } diff --git a/components/guns/http/base_test.go b/components/guns/http/base_test.go index ea97e6259..767a55279 100644 --- a/components/guns/http/base_test.go +++ b/components/guns/http/base_test.go @@ -14,6 +14,9 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + ammomock "github.com/yandex/pandora/components/guns/http/mocks" "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/aggregator/netsample" @@ -21,8 +24,6 @@ import ( "github.com/yandex/pandora/core/engine" "github.com/yandex/pandora/lib/monitoring" "github.com/yandex/pandora/lib/testutil" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" ) func newLogger() *zap.Logger { @@ -103,9 +104,38 @@ func (a *ammoMock) IsInvalid() bool { return false } +type testDecoratedClient struct { + client Client + t *testing.T + before func(req *http.Request) + after func(req *http.Request, res *http.Response, err error) + returnRes *http.Response + returnErr error +} + +func (c *testDecoratedClient) Do(req *http.Request) (*http.Response, error) { + if c.before != nil { + c.before(req) + } + if c.client == nil { + return c.returnRes, c.returnErr + } + res, err := c.client.Do(req) + if c.after != nil { + c.after(req, res, err) + } + return res, err +} + +func (c *testDecoratedClient) CloseIdleConnections() { + c.client.CloseIdleConnections() +} + func (s *BaseGunSuite) Test_Shoot_BeforeBindPanics() { - s.base.Do = func(*http.Request) (_ *http.Response, _ error) { - panic("should not be called") + s.base.client = &testDecoratedClient{ + client: s.base.client, + before: func(req *http.Request) { panic("should not be called\"") }, + after: nil, } am := &ammoMock{} @@ -121,7 +151,6 @@ func (s *BaseGunSuite) Test_Shoot() { am *ammomock.Ammo req *http.Request tag string - res *http.Response sample *netsample.Sample results *netsample.TestAggregator shootErr error @@ -138,11 +167,7 @@ func (s *BaseGunSuite) Test_Shoot() { justBeforeEach := func() { sample = netsample.Acquire(tag) am.On("Request").Return(req, sample).Maybe() - res = &http.Response{ - StatusCode: http.StatusNotFound, - Body: ioutil.NopCloser(body), - Request: req, - } + s.base.Shoot(am) s.Require().Len(results.Samples, 1) shootErr = results.Samples[0].Err() @@ -152,9 +177,15 @@ func (s *BaseGunSuite) Test_Shoot() { beforeEachDoOk := func() { body = ioutil.NopCloser(strings.NewReader("aaaaaaa")) s.base.AnswLog = zap.NewNop() - s.base.Do = func(doReq *http.Request) (*http.Response, error) { - s.Require().Equal(req, doReq) - return res, nil + s.base.client = &testDecoratedClient{ + before: func(doReq *http.Request) { + s.Require().Equal(req, doReq) + }, + returnRes: &http.Response{ + StatusCode: http.StatusNotFound, + Body: ioutil.NopCloser(body), + Request: req, + }, } } s.Run("ammo sample sent to results", func() { @@ -166,7 +197,7 @@ func (s *BaseGunSuite) Test_Shoot() { s.Assert().Len(results.Samples, 1) s.Assert().Equal(sample, results.Samples[0]) s.Assert().Equal("__EMPTY__", sample.Tags()) - s.Assert().Equal(res.StatusCode, sample.ProtoCode()) + s.Assert().Equal(http.StatusNotFound, sample.ProtoCode()) _ = shootErr }) @@ -232,10 +263,12 @@ func (s *BaseGunSuite) Test_Shoot() { connectCalled = true return nil } - oldDo := s.base.Do - s.base.Do = func(r *http.Request) (*http.Response, error) { - doCalled = true - return oldDo(r) + + s.base.client = &testDecoratedClient{ + client: s.base.client, + before: func(doReq *http.Request) { + doCalled = true + }, } } s.Run("Connect called", func() { diff --git a/components/guns/http/client.go b/components/guns/http/client.go index 94821d56d..1ae3cde75 100644 --- a/components/guns/http/client.go +++ b/components/guns/http/client.go @@ -8,9 +8,10 @@ import ( "time" "github.com/pkg/errors" - "github.com/yandex/pandora/lib/netutil" "go.uber.org/zap" "golang.org/x/net/http2" + + "github.com/yandex/pandora/lib/netutil" ) //go:generate mockery --name=Client --case=underscore --inpackage --testonly @@ -21,11 +22,14 @@ type Client interface { } type ClientConfig struct { - Redirect bool // When true, follow HTTP redirects. - Dialer DialerConfig `config:"dial"` - Transport TransportConfig `config:",squash"` + Redirect bool // When true, follow HTTP redirects. + Dialer DialerConfig `config:"dial"` + Transport TransportConfig `config:",squash"` + ConnectSSL bool `config:"connect-ssl"` // Defines if tunnel encrypted. } +type clientConstructor func(clientConfig ClientConfig, target string) Client + func DefaultClientConfig() ClientConfig { return ClientConfig{ Transport: DefaultTransportConfig(), @@ -170,6 +174,29 @@ func (c *panicOnHTTP1Client) Do(req *http.Request) (*http.Response, error) { return res, nil } +type httpDecoratedClient struct { + client Client + scheme string + hostname string + targetResolved string +} + +func (c *httpDecoratedClient) Do(req *http.Request) (*http.Response, error) { + if req.Host == "" { + req.Host = c.hostname + } + + if c.targetResolved != "" { + req.URL.Host = c.targetResolved + } + req.URL.Scheme = c.scheme + return c.client.Do(req) +} + +func (c *httpDecoratedClient) CloseIdleConnections() { + c.client.CloseIdleConnections() +} + func checkHTTP2(state *tls.ConnectionState) error { if state == nil { return errors.New("http2: non TLS connection") diff --git a/components/guns/http/connect.go b/components/guns/http/connect.go index e3416c86e..813ad938b 100644 --- a/components/guns/http/connect.go +++ b/components/guns/http/connect.go @@ -10,70 +10,52 @@ import ( "net/url" "github.com/pkg/errors" - "github.com/yandex/pandora/lib/netutil" "go.uber.org/zap" -) -type ConnectGunConfig struct { - Target string `validate:"endpoint,required"` - ConnectSSL bool `config:"connect-ssl"` // Defines if tunnel encrypted. - SSL bool // As in HTTP gun, defines scheme for http requests. - Client ClientConfig `config:",squash"` - BaseGunConfig `config:",squash"` -} + "github.com/yandex/pandora/lib/netutil" +) -func NewConnectGun(conf ConnectGunConfig, answLog *zap.Logger) *ConnectGun { +func NewConnectGun(cfg HTTPGunConfig, answLog *zap.Logger) *BaseGun { scheme := "http" - if conf.SSL { + if cfg.SSL { scheme = "https" } - client := newConnectClient(conf) - var g ConnectGun - g = ConnectGun{ - BaseGun: BaseGun{ - Config: conf.BaseGunConfig, - Do: g.Do, - OnClose: func() error { - client.CloseIdleConnections() - return nil - }, - AnswLog: answLog, + client := newConnectClient(cfg.Client, cfg.Target) + wrappedClient := &httpDecoratedClient{ + client: client, + scheme: scheme, + hostname: "", + targetResolved: cfg.Target, + } + return &BaseGun{ + Config: cfg.Base, + OnClose: func() error { + client.CloseIdleConnections() + return nil }, - scheme: scheme, - client: client, + AnswLog: answLog, + client: wrappedClient, } - return &g -} - -type ConnectGun struct { - BaseGun - scheme string - client Client -} - -var _ Gun = (*ConnectGun)(nil) - -func (g *ConnectGun) Do(req *http.Request) (*http.Response, error) { - req.URL.Scheme = g.scheme - return g.client.Do(req) } -func DefaultConnectGunConfig() ConnectGunConfig { - return ConnectGunConfig{ - SSL: false, - ConnectSSL: false, - Client: DefaultClientConfig(), +func DefaultConnectGunConfig() HTTPGunConfig { + return HTTPGunConfig{ + SSL: false, + Client: DefaultClientConfig(), + Base: DefaultBaseGunConfig(), } } -func newConnectClient(conf ConnectGunConfig) Client { - transport := NewTransport(conf.Client.Transport, +var newConnectClient clientConstructor = func(conf ClientConfig, target string) Client { + transport := NewTransport( + conf.Transport, newConnectDialFunc( - conf.Target, + target, conf.ConnectSSL, - NewDialer(conf.Client.Dialer), - ), conf.Target) - return newClient(transport, conf.Client.Redirect) + NewDialer(conf.Dialer), + ), + target) + return newClient(transport, conf.Redirect) } func newConnectDialFunc(target string, connectSSL bool, dialer netutil.Dialer) netutil.DialerFunc { diff --git a/components/guns/http/connect_test.go b/components/guns/http/connect_test.go index e83955d2c..3f5ec6ad2 100644 --- a/components/guns/http/connect_test.go +++ b/components/guns/http/connect_test.go @@ -14,12 +14,14 @@ import ( "go.uber.org/zap" ) -var tunnelHandler = func(t *testing.T, originURL string) http.Handler { +var tunnelHandler = func(t *testing.T, originURL string, compareURI bool) http.Handler { u, err := url.Parse(originURL) require.NoError(t, err) originHost := u.Host return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, originHost, r.RequestURI) + if compareURI { + require.Equal(t, originHost, r.RequestURI) + } toOrigin, err := net.Dial("tcp", originHost) require.NoError(t, err) @@ -60,9 +62,9 @@ func TestDo(t *testing.T) { var proxy *httptest.Server if tunnelSSL { - proxy = httptest.NewTLSServer(tunnelHandler(t, origin.URL)) + proxy = httptest.NewTLSServer(tunnelHandler(t, origin.URL, true)) } else { - proxy = httptest.NewServer(tunnelHandler(t, origin.URL)) + proxy = httptest.NewServer(tunnelHandler(t, origin.URL, true)) } defer proxy.Close() @@ -70,14 +72,14 @@ func TestDo(t *testing.T) { require.NoError(t, err) conf := DefaultConnectGunConfig() - conf.ConnectSSL = tunnelSSL + conf.Client.ConnectSSL = tunnelSSL scheme := "http://" if tunnelSSL { scheme = "https://" } conf.Target = strings.TrimPrefix(proxy.URL, scheme) - client := newConnectClient(conf) + client := newConnectClient(conf.Client, conf.Target) res, err := client.Do(req) require.NoError(t, err) @@ -91,7 +93,7 @@ func TestNewConnectGun(t *testing.T) { rw.WriteHeader(http.StatusOK) })) defer origin.Close() - proxy := httptest.NewServer(tunnelHandler(t, origin.URL)) + proxy := httptest.NewServer(tunnelHandler(t, origin.URL, false)) defer proxy.Close() log := zap.NewNop() diff --git a/components/guns/http/http.go b/components/guns/http/http.go index ddf8b9fd8..6ddab579e 100644 --- a/components/guns/http/http.go +++ b/components/guns/http/http.go @@ -1,110 +1,81 @@ package phttp import ( - "net/http" - "github.com/pkg/errors" "go.uber.org/zap" ) -type ClientGunConfig struct { - Target string `validate:"endpoint,required"` - SSL bool - Base BaseGunConfig `config:",squash"` -} - type HTTPGunConfig struct { - Gun ClientGunConfig `config:",squash"` - Client ClientConfig `config:",squash"` + Base BaseGunConfig `config:",squash"` + Client ClientConfig `config:",squash"` + Target string `validate:"endpoint,required"` + SSL bool } -type HTTP2GunConfig struct { - Gun ClientGunConfig `config:",squash"` - Client ClientConfig `config:",squash"` +func NewHTTP1Gun(conf HTTPGunConfig, answLog *zap.Logger, targetResolved string) *BaseGun { + return newHTTPGun(HTTP1ClientConstructor, conf, answLog, targetResolved) } -func NewHTTPGun(conf HTTPGunConfig, answLog *zap.Logger, targetResolved string) *HTTPGun { - transport := NewTransport(conf.Client.Transport, NewDialer(conf.Client.Dialer).DialContext, conf.Gun.Target) - client := newClient(transport, conf.Client.Redirect) - return NewClientGun(client, conf.Gun, answLog, targetResolved) +var HTTP1ClientConstructor clientConstructor = func(clientConfig ClientConfig, target string) Client { + transport := NewTransport(clientConfig.Transport, NewDialer(clientConfig.Dialer).DialContext, target) + client := newClient(transport, clientConfig.Redirect) + return client } // NewHTTP2Gun return simple HTTP/2 gun that can shoot sequentially through one connection. -func NewHTTP2Gun(conf HTTP2GunConfig, answLog *zap.Logger, targetResolved string) (*HTTPGun, error) { - if !conf.Gun.SSL { +func NewHTTP2Gun(conf HTTPGunConfig, answLog *zap.Logger, targetResolved string) (*BaseGun, error) { + if !conf.SSL { // Open issue on github if you really need this feature. return nil, errors.New("HTTP/2.0 over TCP is not supported. Please leave SSL option true by default.") } - transport := NewHTTP2Transport(conf.Client.Transport, NewDialer(conf.Client.Dialer).DialContext, conf.Gun.Target) - client := newClient(transport, conf.Client.Redirect) + return newHTTPGun(HTTP2ClientConstructor, conf, answLog, targetResolved), nil +} + +var HTTP2ClientConstructor clientConstructor = func(clientConfig ClientConfig, target string) Client { + transport := NewHTTP2Transport(clientConfig.Transport, NewDialer(clientConfig.Dialer).DialContext, target) + client := newClient(transport, clientConfig.Redirect) // Will panic and cancel shooting whet target doesn't support HTTP/2. - client = &panicOnHTTP1Client{client} - return NewClientGun(client, conf.Gun, answLog, targetResolved), nil + return &panicOnHTTP1Client{Client: client} } -func NewClientGun(client Client, conf ClientGunConfig, answLog *zap.Logger, targetResolved string) *HTTPGun { +func newHTTPGun(clientConstructor clientConstructor, cfg HTTPGunConfig, answLog *zap.Logger, targetResolved string) *BaseGun { scheme := "http" - if conf.SSL { + if cfg.SSL { scheme = "https" } - var g HTTPGun - g = HTTPGun{ - BaseGun: BaseGun{ - Config: conf.Base, - Do: g.Do, - OnClose: func() error { - client.CloseIdleConnections() - return nil - }, - AnswLog: answLog, - }, - scheme: scheme, - hostname: getHostWithoutPort(conf.Target), - targetResolved: targetResolved, + client := clientConstructor(cfg.Client, cfg.Target) + wrappedClient := &httpDecoratedClient{ client: client, + hostname: getHostWithoutPort(cfg.Target), + targetResolved: targetResolved, + scheme: scheme, } - return &g -} - -type HTTPGun struct { - BaseGun - scheme string - hostname string - targetResolved string - client Client -} - -var _ Gun = (*HTTPGun)(nil) + return &BaseGun{ + Config: cfg.Base, + OnClose: func() error { + client.CloseIdleConnections() + return nil + }, + AnswLog: answLog, -func (g *HTTPGun) Do(req *http.Request) (*http.Response, error) { - if req.Host == "" { - req.Host = g.hostname + hostname: getHostWithoutPort(cfg.Target), + targetResolved: targetResolved, + client: wrappedClient, } - - req.URL.Host = g.targetResolved - req.URL.Scheme = g.scheme - return g.client.Do(req) } func DefaultHTTPGunConfig() HTTPGunConfig { return HTTPGunConfig{ - Gun: DefaultClientGunConfig(), + SSL: false, + Base: DefaultBaseGunConfig(), Client: DefaultClientConfig(), } } -func DefaultHTTP2GunConfig() HTTP2GunConfig { - conf := HTTP2GunConfig{ +func DefaultHTTP2GunConfig() HTTPGunConfig { + return HTTPGunConfig{ Client: DefaultClientConfig(), - Gun: DefaultClientGunConfig(), - } - conf.Gun.SSL = true - return conf -} - -func DefaultClientGunConfig() ClientGunConfig { - return ClientGunConfig{ - SSL: false, - Base: DefaultBaseGunConfig(), + Base: DefaultBaseGunConfig(), + SSL: true, } } diff --git a/components/guns/http/http_test.go b/components/guns/http/http_test.go index e03f1ad9a..8198513d8 100644 --- a/components/guns/http/http_test.go +++ b/components/guns/http/http_test.go @@ -8,12 +8,13 @@ import ( "testing" "github.com/stretchr/testify/require" - ammomock "github.com/yandex/pandora/components/guns/http/mocks" - "github.com/yandex/pandora/core/aggregator/netsample" - "github.com/yandex/pandora/core/config" "go.uber.org/atomic" "go.uber.org/zap" "golang.org/x/net/http2" + + ammomock "github.com/yandex/pandora/components/guns/http/mocks" + "github.com/yandex/pandora/core/aggregator/netsample" + "github.com/yandex/pandora/core/config" ) func TestBaseGun_GunClientConfig_decode(t *testing.T) { @@ -39,10 +40,10 @@ func TestBaseGun_integration(t *testing.T) { defer server.Close() log := zap.NewNop() conf := DefaultHTTPGunConfig() - conf.Gun.Target = host + ":80" + conf.Target = host + ":80" targetResolved := strings.TrimPrefix(server.URL, "http://") results := &netsample.TestAggregator{} - httpGun := NewHTTPGun(conf, log, targetResolved) + httpGun := NewHTTP1Gun(conf, log, targetResolved) _ = httpGun.Bind(results, testDeps()) am := newAmmoReq(t, expectedReq) @@ -88,9 +89,9 @@ func TestHTTP(t *testing.T) { defer server.Close() log := zap.NewNop() conf := DefaultHTTPGunConfig() - conf.Gun.Target = server.Listener.Addr().String() - conf.Gun.SSL = tt.https - gun := NewHTTPGun(conf, log, conf.Gun.Target) + conf.Target = server.Listener.Addr().String() + conf.SSL = tt.https + gun := NewHTTP1Gun(conf, log, conf.Target) var aggr netsample.TestAggregator _ = gun.Bind(&aggr, testDeps()) gun.Shoot(newAmmoURL(t, "/")) @@ -129,9 +130,9 @@ func TestHTTP_Redirect(t *testing.T) { defer server.Close() log := zap.NewNop() conf := DefaultHTTPGunConfig() - conf.Gun.Target = server.Listener.Addr().String() + conf.Target = server.Listener.Addr().String() conf.Client.Redirect = tt.redirect - gun := NewHTTPGun(conf, log, conf.Gun.Target) + gun := NewHTTP1Gun(conf, log, conf.Target) var aggr netsample.TestAggregator _ = gun.Bind(&aggr, testDeps()) gun.Shoot(newAmmoURL(t, "/redirect")) @@ -167,9 +168,9 @@ func TestHTTP_notSupportHTTP2(t *testing.T) { log := zap.NewNop() conf := DefaultHTTPGunConfig() - conf.Gun.Target = server.Listener.Addr().String() - conf.Gun.SSL = true - gun := NewHTTPGun(conf, log, conf.Gun.Target) + conf.Target = server.Listener.Addr().String() + conf.SSL = true + gun := NewHTTP1Gun(conf, log, conf.Target) var results netsample.TestAggregator _ = gun.Bind(&results, testDeps()) gun.Shoot(newAmmoURL(t, "/")) @@ -190,8 +191,8 @@ func TestHTTP2(t *testing.T) { defer server.Close() log := zap.NewNop() conf := DefaultHTTP2GunConfig() - conf.Gun.Target = server.Listener.Addr().String() - gun, _ := NewHTTP2Gun(conf, log, conf.Gun.Target) + conf.Target = server.Listener.Addr().String() + gun, _ := NewHTTP2Gun(conf, log, conf.Target) var results netsample.TestAggregator _ = gun.Bind(&results, testDeps()) gun.Shoot(newAmmoURL(t, "/")) @@ -205,8 +206,8 @@ func TestHTTP2(t *testing.T) { defer server.Close() log := zap.NewNop() conf := DefaultHTTP2GunConfig() - conf.Gun.Target = server.Listener.Addr().String() - gun, _ := NewHTTP2Gun(conf, log, conf.Gun.Target) + conf.Target = server.Listener.Addr().String() + gun, _ := NewHTTP2Gun(conf, log, conf.Target) var results netsample.TestAggregator _ = gun.Bind(&results, testDeps()) var r interface{} @@ -227,9 +228,9 @@ func TestHTTP2(t *testing.T) { defer server.Close() log := zap.NewNop() conf := DefaultHTTP2GunConfig() - conf.Gun.Target = server.Listener.Addr().String() - conf.Gun.SSL = false - _, err := NewHTTP2Gun(conf, log, conf.Gun.Target) + conf.Target = server.Listener.Addr().String() + conf.SSL = false + _, err := NewHTTP2Gun(conf, log, conf.Target) require.Error(t, err) }) } diff --git a/components/guns/http/core.go b/components/guns/http/wrapper.go similarity index 86% rename from components/guns/http/core.go rename to components/guns/http/wrapper.go index 1a60ba5a3..0ddcad5b5 100644 --- a/components/guns/http/core.go +++ b/components/guns/http/wrapper.go @@ -5,6 +5,7 @@ import ( "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/aggregator/netsample" + "github.com/yandex/pandora/core/warmup" ) //go:generate mockery --name=Ammo --case=underscore --outpkg=ammomock @@ -24,6 +25,7 @@ type Ammo interface { type Gun interface { Shoot(ammo Ammo) Bind(sample netsample.Aggregator, deps core.GunDeps) error + WarmUp(opts *warmup.Options) (any, error) } func WrapGun(g Gun) core.Gun { @@ -42,3 +44,7 @@ func (g *gunWrapper) Shoot(ammo core.Ammo) { func (g *gunWrapper) Bind(a core.Aggregator, deps core.GunDeps) error { return g.Gun.Bind(netsample.UnwrapAggregator(a), deps) } + +func (g *gunWrapper) WarmUp(opts *warmup.Options) (any, error) { + return g.Gun.WarmUp(opts) +} diff --git a/components/guns/http_scenario/gun.go b/components/guns/http_scenario/gun.go index 22b689329..c6136dd85 100644 --- a/components/guns/http_scenario/gun.go +++ b/components/guns/http_scenario/gun.go @@ -13,10 +13,11 @@ import ( "strings" "time" + "go.uber.org/zap" + phttp "github.com/yandex/pandora/components/guns/http" "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/aggregator/netsample" - "go.uber.org/zap" ) type Gun interface { @@ -64,7 +65,7 @@ func (g *BaseGun) Bind(aggregator netsample.Aggregator, deps core.GunDeps) error return nil } -// Shoot is thread safe iff Do and Connect hooks are thread safe. +// Shoot is thread safe if Do and Connect hooks are thread safe. func (g *BaseGun) Shoot(ammo *Scenario) { if g.Aggregator == nil { zap.L().Panic("must bind before shoot") @@ -176,7 +177,7 @@ func (g *BaseGun) shootStep(step Request, sample *netsample.Sample, ammoName str timings, req := g.initTracing(req, sample) - resp, err := g.Do(req) + resp, err := g.client.Do(req) g.saveTrace(timings, sample, resp) diff --git a/components/guns/http_scenario/import.go b/components/guns/http_scenario/import.go index fec37771c..19c0fe13b 100644 --- a/components/guns/http_scenario/import.go +++ b/components/guns/http_scenario/import.go @@ -4,13 +4,14 @@ import ( "net" "github.com/spf13/afero" + "go.uber.org/zap" + phttp "github.com/yandex/pandora/components/guns/http" "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/aggregator/netsample" "github.com/yandex/pandora/core/register" "github.com/yandex/pandora/lib/answlog" "github.com/yandex/pandora/lib/netutil" - "go.uber.org/zap" ) func WrapGun(g Gun) core.Gun { @@ -34,17 +35,17 @@ func (g *gunWrapper) Bind(a core.Aggregator, deps core.GunDeps) error { func Import(fs afero.Fs) { register.Gun("http/scenario", func(conf phttp.HTTPGunConfig) func() core.Gun { - targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) + targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Target) + answLog := answlog.Init(conf.Base.AnswLog.Path, conf.Base.AnswLog.Enabled) return func() core.Gun { gun := NewHTTPGun(conf, answLog, targetResolved) return WrapGun(gun) } }, phttp.DefaultHTTPGunConfig) - register.Gun("http2/scenario", func(conf phttp.HTTP2GunConfig) func() (core.Gun, error) { - targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) + register.Gun("http2/scenario", func(conf phttp.HTTPGunConfig) func() (core.Gun, error) { + targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Target) + answLog := answlog.Init(conf.Base.AnswLog.Path, conf.Base.AnswLog.Enabled) return func() (core.Gun, error) { gun, err := NewHTTP2Gun(conf, answLog, targetResolved) return WrapGun(gun), err diff --git a/components/guns/http_scenario/new.go b/components/guns/http_scenario/new.go index e65fbcdbf..f2acb9db2 100644 --- a/components/guns/http_scenario/new.go +++ b/components/guns/http_scenario/new.go @@ -4,30 +4,31 @@ import ( "errors" "net" - phttp "github.com/yandex/pandora/components/guns/http" "go.uber.org/zap" + + phttp "github.com/yandex/pandora/components/guns/http" ) func NewHTTPGun(conf phttp.HTTPGunConfig, answLog *zap.Logger, targetResolved string) *BaseGun { - transport := phttp.NewTransport(conf.Client.Transport, phttp.NewDialer(conf.Client.Dialer).DialContext, conf.Gun.Target) + transport := phttp.NewTransport(conf.Client.Transport, phttp.NewDialer(conf.Client.Dialer).DialContext, conf.Target) client := newClient(transport, conf.Client.Redirect) - return NewClientGun(client, conf.Gun, answLog, targetResolved) + return NewClientGun(client, conf, answLog, targetResolved) } // NewHTTP2Gun return simple HTTP/2 gun that can shoot sequentially through one connection. -func NewHTTP2Gun(conf phttp.HTTP2GunConfig, answLog *zap.Logger, targetResolved string) (*BaseGun, error) { - if !conf.Gun.SSL { +func NewHTTP2Gun(conf phttp.HTTPGunConfig, answLog *zap.Logger, targetResolved string) (*BaseGun, error) { + if !conf.SSL { // Open issue on github if you really need this feature. return nil, errors.New("HTTP/2.0 over TCP is not supported. Please leave SSL option true by default") } - transport := phttp.NewHTTP2Transport(conf.Client.Transport, phttp.NewDialer(conf.Client.Dialer).DialContext, conf.Gun.Target) + transport := phttp.NewHTTP2Transport(conf.Client.Transport, phttp.NewDialer(conf.Client.Dialer).DialContext, conf.Target) client := newClient(transport, conf.Client.Redirect) // Will panic and cancel shooting whet target doesn't support HTTP/2. client = &panicOnHTTP1Client{client} - return NewClientGun(client, conf.Gun, answLog, targetResolved), nil + return NewClientGun(client, conf, answLog, targetResolved), nil } -func NewClientGun(client Client, conf phttp.ClientGunConfig, answLog *zap.Logger, targetResolved string) *BaseGun { +func NewClientGun(client Client, conf phttp.HTTPGunConfig, answLog *zap.Logger, targetResolved string) *BaseGun { scheme := "http" if conf.SSL { scheme = "https" diff --git a/components/phttp/import/import.go b/components/phttp/import/import.go index ff1316810..7c5e67f3c 100644 --- a/components/phttp/import/import.go +++ b/components/phttp/import/import.go @@ -4,6 +4,8 @@ import ( "net" "github.com/spf13/afero" + "go.uber.org/zap" + phttp "github.com/yandex/pandora/components/guns/http" scenarioGun "github.com/yandex/pandora/components/guns/http_scenario" httpProvider "github.com/yandex/pandora/components/providers/http" @@ -12,7 +14,6 @@ import ( "github.com/yandex/pandora/core/register" "github.com/yandex/pandora/lib/answlog" "github.com/yandex/pandora/lib/netutil" - "go.uber.org/zap" ) func Import(fs afero.Fs) { @@ -21,23 +22,23 @@ func Import(fs afero.Fs) { scenarioProvider.Import(fs) register.Gun("http", func(conf phttp.HTTPGunConfig) func() core.Gun { - targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) - return func() core.Gun { return phttp.WrapGun(phttp.NewHTTPGun(conf, answLog, targetResolved)) } + targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Target) + answLog := answlog.Init(conf.Base.AnswLog.Path, conf.Base.AnswLog.Enabled) + return func() core.Gun { return phttp.WrapGun(phttp.NewHTTP1Gun(conf, answLog, targetResolved)) } }, phttp.DefaultHTTPGunConfig) - register.Gun("http2", func(conf phttp.HTTP2GunConfig) func() (core.Gun, error) { - targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) + register.Gun("http2", func(conf phttp.HTTPGunConfig) func() (core.Gun, error) { + targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Target) + answLog := answlog.Init(conf.Base.AnswLog.Path, conf.Base.AnswLog.Enabled) return func() (core.Gun, error) { gun, err := phttp.NewHTTP2Gun(conf, answLog, targetResolved) return phttp.WrapGun(gun), err } }, phttp.DefaultHTTP2GunConfig) - register.Gun("connect", func(conf phttp.ConnectGunConfig) func() core.Gun { + register.Gun("connect", func(conf phttp.HTTPGunConfig) func() core.Gun { conf.Target, _ = PreResolveTargetAddr(&conf.Client, conf.Target) - answLog := answlog.Init(conf.BaseGunConfig.AnswLog.Path, conf.BaseGunConfig.AnswLog.Enabled) + answLog := answlog.Init(conf.Base.AnswLog.Path, conf.Base.AnswLog.Enabled) return func() core.Gun { return phttp.WrapGun(phttp.NewConnectGun(conf, answLog)) } diff --git a/tests/acceptance/connect_test.go b/tests/acceptance/connect_test.go new file mode 100644 index 000000000..9b8e066e7 --- /dev/null +++ b/tests/acceptance/connect_test.go @@ -0,0 +1,115 @@ +package acceptance + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/suite" + grpc "github.com/yandex/pandora/components/grpc/import" + phttpimport "github.com/yandex/pandora/components/phttp/import" + "github.com/yandex/pandora/core/engine" + coreimport "github.com/yandex/pandora/core/import" + "github.com/yandex/pandora/lib/testutil" + "go.uber.org/atomic" + "go.uber.org/zap" +) + +func TestConnectGunSuite(t *testing.T) { + suite.Run(t, new(ConnectGunSuite)) +} + +type ConnectGunSuite struct { + suite.Suite + fs afero.Fs + log *zap.Logger + metrics engine.Metrics +} + +func (s *ConnectGunSuite) SetupSuite() { + s.fs = afero.NewOsFs() + testOnce.Do(func() { + coreimport.Import(s.fs) + phttpimport.Import(s.fs) + grpc.Import(s.fs) + }) + + s.log = testutil.NewNullLogger() + s.metrics = newEngineMetrics("connect_suite") +} + +func (s *ConnectGunSuite) Test_Connect() { + tests := []struct { + name string + filecfg string + isTLS bool + preStartSrv func(srv *httptest.Server) + wantErrContain string + wantCnt int + }{ + { + name: "http", + filecfg: "testdata/connect/connect.yaml", + isTLS: false, + wantCnt: 4, + }, + { + name: "http-check-limits", + filecfg: "testdata/connect/connect-check-limit.yaml", + isTLS: false, + wantCnt: 8, + }, + { + name: "http-check-passes", + filecfg: "testdata/connect/connect-check-passes.yaml", + isTLS: false, + wantCnt: 15, + }, + // TODO: first record does not look like a TLS handshake. Check https://go.dev/src/crypto/tls/conn.go + { + name: "connect-ssl", + filecfg: "testdata/connect/connect-ssl.yaml", + isTLS: true, + wantCnt: 4, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + var requetsCount atomic.Int64 // Request served by test server. + requetsCount.Store(0) + srv := httptest.NewUnstartedServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + requetsCount.Inc() + rw.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + conf := parseConfigFile(s.T(), tt.filecfg, srv.Listener.Addr().String()) + s.Require().Equal(1, len(conf.Engine.Pools)) + aggr := &aggregator{} + conf.Engine.Pools[0].Aggregator = aggr + pandora := engine.New(s.log, s.metrics, conf.Engine) + + if tt.preStartSrv != nil { + tt.preStartSrv(srv) + } + if tt.isTLS { + srv.StartTLS() + } else { + srv.Start() + } + err := pandora.Run(context.Background()) + if tt.wantErrContain != "" { + s.Assert().Equal(int64(0), requetsCount.Load()) + s.Require().Error(err) + s.Require().Contains(err.Error(), tt.wantErrContain) + return + } + s.Require().NoError(err) + s.Require().Equal(int64(tt.wantCnt), int64(len(aggr.samples))) + s.Assert().GreaterOrEqual(requetsCount.Load(), int64(len(aggr.samples))) // requetsCount more than shoots + }) + } +} diff --git a/tests/acceptance/testdata/connect/connect-check-limit.yaml b/tests/acceptance/testdata/connect/connect-check-limit.yaml new file mode 100644 index 000000000..cbc77ecac --- /dev/null +++ b/tests/acceptance/testdata/connect/connect-check-limit.yaml @@ -0,0 +1,23 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload5.uri + type: uri + limit: 8 + result: + type: discard + gun: + target: {{.target}} + type: connect + answlog: + enabled: false + rps-per-instance: false + rps: + - duration: 5s + ops: 10 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/connect/connect-check-passes.yaml b/tests/acceptance/testdata/connect/connect-check-passes.yaml new file mode 100644 index 000000000..42aa9f66b --- /dev/null +++ b/tests/acceptance/testdata/connect/connect-check-passes.yaml @@ -0,0 +1,23 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload5.uri + type: uri + passes: 3 + result: + type: discard + gun: + target: {{.target}} + type: connect + answlog: + enabled: false + rps-per-instance: false + rps: + - duration: 5s + ops: 10 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/connect/connect-ssl.yaml b/tests/acceptance/testdata/connect/connect-ssl.yaml new file mode 100644 index 000000000..d36d9d89d --- /dev/null +++ b/tests/acceptance/testdata/connect/connect-ssl.yaml @@ -0,0 +1,26 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload.uri + type: uri + result: + type: discard + gun: + target: {{.target}} + type: connect + ssl: true + connect-ssl: true + answlog: + enabled: false + rps-per-instance: false + rps: + - times: 2 + type: once + - duration: 0.5s + ops: 4 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/connect/connect.yaml b/tests/acceptance/testdata/connect/connect.yaml new file mode 100644 index 000000000..323a9cfd2 --- /dev/null +++ b/tests/acceptance/testdata/connect/connect.yaml @@ -0,0 +1,24 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload.uri + type: uri + result: + type: discard + gun: + target: {{.target}} + type: connect + answlog: + enabled: false + rps-per-instance: false + rps: + - times: 2 + type: once + - duration: 0.5s + ops: 4 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/connect/payload.uri b/tests/acceptance/testdata/connect/payload.uri new file mode 100644 index 000000000..35ec3b9d7 --- /dev/null +++ b/tests/acceptance/testdata/connect/payload.uri @@ -0,0 +1 @@ +/ \ No newline at end of file diff --git a/tests/acceptance/testdata/connect/payload5.uri b/tests/acceptance/testdata/connect/payload5.uri new file mode 100644 index 000000000..760465f78 --- /dev/null +++ b/tests/acceptance/testdata/connect/payload5.uri @@ -0,0 +1,5 @@ +/a +/b +/c +/d +/e diff --git a/tests/http_scenario/main_test.go b/tests/http_scenario/main_test.go index b28350d8c..cbb0b0720 100644 --- a/tests/http_scenario/main_test.go +++ b/tests/http_scenario/main_test.go @@ -11,6 +11,8 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "go.uber.org/zap" + phttp "github.com/yandex/pandora/components/guns/http" httpscenario "github.com/yandex/pandora/components/guns/http_scenario" ammo "github.com/yandex/pandora/components/providers/scenario" @@ -20,7 +22,6 @@ import ( "github.com/yandex/pandora/core/aggregator/netsample" "github.com/yandex/pandora/core/plugin/pluginconfig" "github.com/yandex/pandora/examples/http/server" - "go.uber.org/zap" ) var testOnce = &sync.Once{} @@ -73,9 +74,7 @@ func (s *GunSuite) Test_SuccessScenario() { ctx := context.Background() log := zap.NewNop() g := httpscenario.NewHTTPGun(phttp.HTTPGunConfig{ - Gun: phttp.ClientGunConfig{ - Target: s.addr, - }, + Target: s.addr, Client: phttp.ClientConfig{}, }, log, s.addr)