Skip to content

feat: add load testing harness, coder loadtest command #4853

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

Merged
merged 7 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fixup! feat: add coder loadtest command
  • Loading branch information
deansheather committed Nov 2, 2022
commit df7866cfe0a4ab362f23efc1464ef3295a3aa006
2 changes: 1 addition & 1 deletion cli/loadtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func loadtest() *cobra.Command {
testCtx := cmd.Context()
if config.Timeout > 0 {
var cancel func()
testCtx, cancel = context.WithTimeout(testCtx, config.Timeout)
testCtx, cancel = context.WithTimeout(testCtx, time.Duration(config.Timeout))
defer cancel()
}

Expand Down
7 changes: 4 additions & 3 deletions cli/loadtest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/loadtest/placebo"
"github.com/coder/coder/loadtest/workspacebuild"
Expand All @@ -40,11 +41,11 @@ func TestLoadTest(t *testing.T) {
Type: cli.LoadTestTypePlacebo,
Count: 10,
Placebo: &placebo.Config{
Sleep: 10 * time.Millisecond,
Sleep: httpapi.Duration(10 * time.Millisecond),
},
},
},
Timeout: 1 * time.Second,
Timeout: httpapi.Duration(testutil.WaitShort),
}

configBytes, err := json.Marshal(config)
Expand Down Expand Up @@ -99,7 +100,7 @@ func TestLoadTest(t *testing.T) {
},
},
},
Timeout: 10 * time.Second,
Timeout: httpapi.Duration(testutil.WaitLong),
}

d := t.TempDir()
Expand Down
7 changes: 4 additions & 3 deletions cli/loadtestconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"golang.org/x/xerrors"

"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/loadtest/harness"
"github.com/coder/coder/loadtest/placebo"
Expand All @@ -17,7 +18,7 @@ type LoadTestConfig struct {
Tests []LoadTest `json:"tests"`
// Timeout sets a timeout for the entire test run, to control the timeout
// for each individual run use strategy.timeout.
Timeout time.Duration `json:"timeout"`
Timeout httpapi.Duration `json:"timeout"`
}

type LoadTestStrategyType string
Expand All @@ -44,7 +45,7 @@ type LoadTestStrategy struct {
// Timeout is the maximum amount of time to run each test for. This is
// independent of the timeout specified in the test run. A timeout of 0
// disables the timeout.
Timeout time.Duration `json:"timeout"`
Timeout httpapi.Duration `json:"timeout"`
}

func (s LoadTestStrategy) ExecutionStrategy() harness.ExecutionStrategy {
Expand All @@ -69,7 +70,7 @@ func (s LoadTestStrategy) ExecutionStrategy() harness.ExecutionStrategy {

if s.Timeout > 0 {
strategy = harness.TimeoutExecutionStrategyWrapper{
Timeout: s.Timeout,
Timeout: time.Duration(s.Timeout),
Inner: strategy,
}
}
Expand Down
45 changes: 45 additions & 0 deletions coderd/httpapi/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package httpapi

import (
"encoding/json"
"time"

"golang.org/x/xerrors"
)

// Duration wraps time.Duration and provides better JSON marshaling and
// unmarshaling.
type Duration time.Duration

var _ json.Marshaler = Duration(0)
var _ json.Unmarshaler = (*Duration)(nil)

// MarshalJSON implements json.Marshaler.
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}

// UnmarshalJSON implements json.Unmarshaler.
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
err := json.Unmarshal(b, &v)
if err != nil {
return xerrors.Errorf("unmarshal JSON value: %w", err)
}

switch value := v.(type) {
case float64:
*d = Duration(time.Duration(value))
return nil
case string:
tmp, err := time.ParseDuration(value)
if err != nil {
return xerrors.Errorf("parse duration %q: %w", value, err)
}

*d = Duration(tmp)
return nil
}

return xerrors.New("invalid duration")
}
168 changes: 168 additions & 0 deletions coderd/httpapi/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package httpapi_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/httpapi"
)

func TestDuration(t *testing.T) {
t.Parallel()

t.Run("MarshalJSON", func(t *testing.T) {
t.Parallel()

cases := []struct {
value time.Duration
expected string
}{
{
value: 0,
expected: "0s",
},
{
value: 1 * time.Millisecond,
expected: "1ms",
},
{
value: 1 * time.Second,
expected: "1s",
},
{
value: 1 * time.Minute,
expected: "1m0s",
},
{
value: 1 * time.Hour,
expected: "1h0m0s",
},
{
value: 1*time.Hour + 1*time.Minute + 1*time.Second + 1*time.Millisecond,
expected: "1h1m1.001s",
},
}

for _, c := range cases {
c := c

t.Run(c.expected, func(t *testing.T) {
t.Parallel()

d := httpapi.Duration(c.value)
b, err := d.MarshalJSON()
require.NoError(t, err)
require.Equal(t, `"`+c.expected+`"`, string(b))
})
}
})

t.Run("UnmarshalJSON", func(t *testing.T) {
t.Parallel()

cases := []struct {
value string
expected time.Duration
}{
{
value: "0ms",
expected: 0,
},
{
value: "0s",
expected: 0,
},
{
value: "1ms",
expected: 1 * time.Millisecond,
},
{
value: "1s",
expected: 1 * time.Second,
},
{
value: "1m",
expected: 1 * time.Minute,
},
{
value: "1m0s",
expected: 1 * time.Minute,
},
{
value: "1h",
expected: 1 * time.Hour,
},
{
value: "1h0m0s",
expected: 1 * time.Hour,
},
{
value: "1h1m1.001s",
expected: 1*time.Hour + 1*time.Minute + 1*time.Second + 1*time.Millisecond,
},
{
value: "1h1m1s1ms",
expected: 1*time.Hour + 1*time.Minute + 1*time.Second + 1*time.Millisecond,
},
}

for _, c := range cases {
c := c

t.Run(c.value, func(t *testing.T) {
t.Parallel()

var d httpapi.Duration
err := d.UnmarshalJSON([]byte(`"` + c.value + `"`))
require.NoError(t, err)
require.Equal(t, c.expected, time.Duration(d))
})
}
})

t.Run("UnmarshalJSONInt", func(t *testing.T) {
t.Parallel()

var d httpapi.Duration
err := d.UnmarshalJSON([]byte("12345"))
require.NoError(t, err)
require.EqualValues(t, 12345, d)
})

t.Run("UnmarshalJSONErrors", func(t *testing.T) {
t.Parallel()

cases := []struct {
value string
errContains string
}{
{
value: "not valid json (no double quotes)",
errContains: "unmarshal JSON value",
},
{
value: `"not valid duration"`,
errContains: "parse duration",
},
{
value: "{}",
errContains: "invalid duration",
},
}

for _, c := range cases {
c := c

t.Run(c.value, func(t *testing.T) {
t.Parallel()

var d httpapi.Duration
err := d.UnmarshalJSON([]byte(c.value))
require.Error(t, err)
require.Contains(t, err.Error(), c.errContains)
})
}
})
}
14 changes: 10 additions & 4 deletions loadtest/placebo/config.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package placebo

import (
"time"

"golang.org/x/xerrors"

"github.com/coder/coder/coderd/httpapi"
)

type Config struct {
// Sleep is how long to sleep for. If unspecified, the test run will finish
// instantly.
Sleep time.Duration `json:"sleep"`
Sleep httpapi.Duration `json:"sleep"`
// Jitter is the maximum amount of jitter to add to the sleep duration. The
// sleep value will be increased by a random value between 0 and jitter if
// jitter is greater than 0.
Jitter time.Duration `json:"jitter"`
Jitter httpapi.Duration `json:"jitter"`
// FailureChance is the chance that the test will fail. The value must be
// between 0 and 1.
FailureChance float64 `json:"failure_chance"`
}

func (c Config) Validate() error {
Expand All @@ -26,6 +29,9 @@ func (c Config) Validate() error {
if c.Jitter > 0 && c.Sleep == 0 {
return xerrors.New("jitter must be 0 if sleep is 0")
}
if c.FailureChance < 0 || c.FailureChance > 1 {
return xerrors.New("failure_chance must be between 0 and 1")
}

return nil
}
Loading