summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2021-03-21 04:30:07 +0700
committerShulhan <ms@kilabit.info>2021-03-21 04:30:07 +0700
commit05474abb07f73aace79bcf339be8e5619dd4564b (patch)
tree6cd71065014161f3f4f95aacb6b0508ce20a216c
parent77e3074ccbbb01945e06d5608d34d5f39352500e (diff)
downloadgorankusu-05474abb07f73aace79bcf339be8e5619dd4564b.tar.xz
all: implement API and interface for attack functionality
When the user click "Attack" it will call the API to run the load testing. If there is load testing currently, it will return with an error. On success, the result of load testing will be stored on directory defined in Environment.ResultsDir with file named <HttpTarget.ID>.<date_time>.<RPS>x<Duration>s.<ResultsSuffix>.bin This file contains the vegeta.Results.
-rw-r--r--.gitignore1
-rw-r--r--_www/index.css3
-rw-r--r--_www/index.js78
-rw-r--r--environment.go20
-rw-r--r--errors.go23
-rw-r--r--example/example.go42
-rw-r--r--http_target.go38
-rw-r--r--key_value.go10
-rw-r--r--load_testing_result.go84
-rw-r--r--run_request.go45
-rw-r--r--target.go11
-rw-r--r--trunks.go135
12 files changed, 410 insertions, 80 deletions
diff --git a/.gitignore b/.gitignore
index 63ecf4f..d2a9400 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/trunks
+/testdata/example
diff --git a/_www/index.css b/_www/index.css
index 0bc5268..9b1336b 100644
--- a/_www/index.css
+++ b/_www/index.css
@@ -23,6 +23,8 @@ body {
padding: 1em;
position: fixed;
width: calc(100% - 18em);
+ height: 100%;
+ overflow: auto;
}
.input {
margin-bottom: 1em;
@@ -42,4 +44,5 @@ body {
border-radius: 1em;
font-family: monospace;
padding: 1em;
+ overflow: auto;
}
diff --git a/_www/index.js b/_www/index.js
index 9691187..c277139 100644
--- a/_www/index.js
+++ b/_www/index.js
@@ -88,24 +88,24 @@ function renderTarget(targetID) {
<h3> Attack options </h3>
<div class="input">
<label> Base URL </label>:
- <input id="BaseUrl" readonly="" value="${target.AttackOpts.BaseUrl}"/>
+ <input id="BaseUrl" readonly="" value="${target.Opts.BaseUrl}"/>
</div>
<div class="input">
<label> Duration </label>:
- <input id="Duration" value="${target.AttackOpts.Duration / 1e9}"/>
+ <input id="Duration" value="${target.Opts.Duration / 1e9}"/>
</div>
<div class="input">
<label> Rate per second </label>:
- <input id="RatePerSecond" value="${target.AttackOpts.RatePerSecond}"/>
+ <input id="RatePerSecond" value="${target.Opts.RatePerSecond}"/>
</div>
<div class="input">
<label> Timeout (seconds) </label>:
- <input id="Timeout" value="${target.AttackOpts.Timeout / 1e9}"/>
+ <input id="Timeout" value="${target.Opts.Timeout / 1e9}"/>
</div>
</div>
`
- if (target.Vars.length > 0) {
+ if (target.Vars && target.Vars.length > 0) {
w += `
<div class='Vars'>
<h3>Variables</h3>
@@ -122,6 +122,7 @@ function renderTarget(targetID) {
}
w += "<div class='HttpTargets'>"
+
for (let x = 0; x < target.HttpTargets.length; x++) {
let http = target.HttpTargets[x]
@@ -132,6 +133,9 @@ function renderTarget(targetID) {
<button onclick="run('${target.ID}', '${http.ID}')">
Run
</button>
+ <button onclick="attack('${target.ID}', '${http.ID}')">
+ Attack
+ </button>
</h3>
<div class="mono">
${_requestMethods[http.Method]} ${http.Path} <br/>
@@ -150,11 +154,18 @@ function renderTarget(targetID) {
}
w += `
- <h4>Response</h4>
- <pre id="${http.ID}_response" class="mono">
- </pre>
- </div>
+ <h4>Run response</h4>
+ <pre id="${http.ID}_response" class="mono">
+ </pre>
`
+
+ if (http.Results && Object.keys(http.Results).length > 0) {
+ w += "<h4>Attack results</h4>"
+ w += renderHttpAttackResults(target, http)
+ }
+
+ w += "</div>"
+
}
w += "</div>"
@@ -195,6 +206,27 @@ function renderHttpTargetParams(target, http) {
return w
}
+function renderHttpAttackResults(target, http) {
+ let w = `<div id="${http.ID}_results">`
+ for (let x = 0; x < http.Results.length; x++) {
+ let result = http.Results[x]
+ w += `
+ <div class="result">
+ <div>${result.Name}</div>
+ <pre class="mono">
+${atob(result.TextReport)}
+ </pre>
+ <pre class="mono">
+${atob(result.HistReport)}
+ </pre>
+ </div>
+ `
+ }
+ w += "</div>"
+ return w
+}
+
+
async function run(targetID, httpTargetID) {
let req = {}
req.Target = _targets[targetID]
@@ -214,6 +246,33 @@ async function run(targetID, httpTargetID) {
elResponse.innerHTML = JSON.stringify(res, null, 2)
}
+async function attack(targetID, httpTargetID) {
+ let target = _targets[targetID]
+ let httpTarget = getHttpTargetByID(target, httpTargetID)
+
+ let req = {
+ Target: {
+ ID: target.ID,
+ Opts: target.Opts,
+ },
+ HttpTarget: {
+ ID: httpTarget.ID,
+ Headers: httpTarget.Headers,
+ Params: httpTarget.Params,
+ },
+ }
+
+ let fres = await fetch("/_trunks/api/target/attack", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(req),
+ })
+
+ let res = await fres.json()
+}
+
function getHttpTargetByID(target, id) {
for (let x = 0; x < target.HttpTargets.length; x++) {
if (id == target.HttpTargets[x].ID) {
@@ -228,6 +287,7 @@ function onChangeHttpHeader(targetID, httpTargetID, key, val) {
let httpTarget = getHttpTargetByID(target, httpTargetID)
httpTarget.Headers[key] = val
}
+
function onChangeHttpParam(targetID, httpTargetID, key, val) {
let target = _targets[targetID]
let httpTarget = getHttpTargetByID(target, httpTargetID)
diff --git a/environment.go b/environment.go
index cd17e56..393741f 100644
--- a/environment.go
+++ b/environment.go
@@ -44,10 +44,10 @@ type Environment struct {
// uniquely identify results on each run.
ResultsSuffix string `ini:"trunks:result:suffix"`
- // LoadTestingRunning will be set to non-nil if there is a load
+ // AttackRunning will be set to non-nil if there is a load
// testing currently running.
- LoadTestingRunning *loadTestingResult
- mtx sync.Mutex
+ AttackRunning *loadTestingResult
+ mtx sync.Mutex
}
func (env *Environment) init() (err error) {
@@ -70,3 +70,17 @@ func (env *Environment) init() (err error) {
return nil
}
+
+func (env *Environment) getRunningAttack() (ltr *loadTestingResult) {
+ env.mtx.Lock()
+ ltr = env.AttackRunning
+ env.mtx.Unlock()
+ return ltr
+}
+
+func (env *Environment) isAttackRunning() (yorn bool) {
+ env.mtx.Lock()
+ yorn = (env.AttackRunning != nil)
+ env.mtx.Unlock()
+ return yorn
+}
diff --git a/errors.go b/errors.go
index 989561a..faf009c 100644
--- a/errors.go
+++ b/errors.go
@@ -12,6 +12,29 @@ import (
libhttp "github.com/shuLhan/share/lib/http"
)
+func errAttackConflict(ltr *loadTestingResult) error {
+ res := &libhttp.EndpointResponse{
+ E: liberrors.E{
+ Code: http.StatusConflict,
+ Message: "another attack is already running",
+ Name: "ERR_ATTACK_CONFLICT",
+ },
+ Data: ltr,
+ }
+ return res
+}
+
+func errAttackNotAllowed() error {
+ res := &libhttp.EndpointResponse{
+ E: liberrors.E{
+ Code: http.StatusNotAcceptable,
+ Message: "attack is not allowed",
+ Name: "ERR_ATTACK_NOT_ALLOWED",
+ },
+ }
+ return res
+}
+
func errInternal(err error) error {
res := &libhttp.EndpointResponse{
E: liberrors.E{
diff --git a/example/example.go b/example/example.go
index 44c3b5b..33c30d1 100644
--- a/example/example.go
+++ b/example/example.go
@@ -10,8 +10,10 @@ import (
"net/http"
"time"
- "git.sr.ht/~shulhan/trunks"
libhttp "github.com/shuLhan/share/lib/http"
+ vegeta "github.com/tsenart/vegeta/v12/lib"
+
+ "git.sr.ht/~shulhan/trunks"
)
const (
@@ -19,7 +21,8 @@ const (
)
type Example struct {
- trunks *trunks.Trunks
+ trunks *trunks.Trunks
+ targetExampleGet vegeta.Target
}
//
@@ -27,7 +30,7 @@ type Example struct {
//
func New() (ex *Example, err error) {
env := &trunks.Environment{
- ResultsDir: "/tmp",
+ ResultsDir: "testdata/example/",
ResultsSuffix: "_trunks_example",
}
@@ -76,7 +79,7 @@ func (ex *Example) registerEndpoints() (err error) {
func (ex *Example) registerTargets() (err error) {
targetHttp := &trunks.Target{
Name: "Example HTTP target",
- AttackOpts: &trunks.AttackOptions{
+ Opts: &trunks.AttackOptions{
BaseUrl: fmt.Sprintf("http://%s", ex.trunks.Env.ListenAddress),
Duration: 5 * time.Second,
RatePerSecond: 10,
@@ -92,7 +95,10 @@ func (ex *Example) registerTargets() (err error) {
Params: trunks.KeyValue{
"Param1": "1",
},
- Run: ex.runExampleGet,
+ Run: ex.runExampleGet,
+ Attack: ex.attackExampleGet,
+ PreAttack: ex.preattackExampleGet,
+ AllowAttack: true,
}},
}
@@ -112,7 +118,7 @@ func (ex *Example) pathExampleGet(epr *libhttp.EndpointRequest) ([]byte, error)
func (ex *Example) runExampleGet(target *trunks.Target, req *trunks.RunRequest) ([]byte, error) {
if target.HttpClient == nil {
- target.HttpClient = libhttp.NewClient(target.AttackOpts.BaseUrl, nil, true)
+ target.HttpClient = libhttp.NewClient(target.Opts.BaseUrl, nil, true)
}
_, resbody, err := target.HttpClient.Get(
req.HttpTarget.Path,
@@ -123,3 +129,27 @@ func (ex *Example) runExampleGet(target *trunks.Target, req *trunks.RunRequest)
}
return resbody, nil
}
+
+func (ex *Example) preattackExampleGet(rr *trunks.RunRequest) {
+ ex.targetExampleGet = vegeta.Target{
+ Method: rr.HttpTarget.Method.String(),
+ URL: fmt.Sprintf("%s%s", rr.Target.Opts.BaseUrl, rr.HttpTarget.Path),
+ Header: rr.HttpTarget.Headers.ToHttpHeader(),
+ }
+
+ q := rr.HttpTarget.Params.ToUrlValues().Encode()
+ if len(q) > 0 {
+ ex.targetExampleGet.URL += "?" + q
+ }
+
+ fmt.Printf("preattackExampleGet: %+v\n", ex.targetExampleGet)
+}
+
+func (ex *Example) attackExampleGet(rr *trunks.RunRequest) vegeta.Targeter {
+ return func(tgt *vegeta.Target) error {
+ rr.HttpTarget.AttackLocker.Lock()
+ *tgt = ex.targetExampleGet
+ rr.HttpTarget.AttackLocker.Unlock()
+ return nil
+ }
+}
diff --git a/http_target.go b/http_target.go
index c69b950..8bff477 100644
--- a/http_target.go
+++ b/http_target.go
@@ -12,16 +12,28 @@ import (
vegeta "github.com/tsenart/vegeta/v12/lib"
)
-type HttpRunHandler func(target *Target, runRequest *RunRequest) ([]byte, error)
-type HttpPreAttackHandler func(target *Target, ht *HttpTarget) vegeta.Targeter
+//
+// HttpRunHandler defiine the function type that will be called when client
+// send request to run the HTTP target.
+//
+type HttpRunHandler func(tgt *Target, rr *RunRequest) ([]byte, error)
+
+//
+// HttpAttackHandler define the function type that will be called when client
+// send request to attack HTTP target.
+//
+type HttpAttackHandler func(rr *RunRequest) vegeta.Targeter
+
+// HttpPreAttackHandler define the function type that will be called before
+// the actual Attack being called.
+type HttpPreAttackHandler func(rr *RunRequest)
type HttpTarget struct {
// ID of target, optional.
// If its empty, it will generated using value from Path.
ID string
- // Name of target, optional.
- // If its empty default to Path.
+ // Name of target, optional, default to Path.
Name string
Method libhttp.RequestMethod
@@ -30,20 +42,18 @@ type HttpTarget struct {
Headers KeyValue
Params KeyValue
- Run HttpRunHandler `json:"-"`
- PreAttack HttpPreAttackHandler `json:"-"`
-
- // Status of REST.
- Status string
+ Run HttpRunHandler `json:"-"`
+ Attack HttpAttackHandler `json:"-"`
+ PreAttack HttpPreAttackHandler `json:"-"`
+ AttackLocker sync.Mutex `json:"-"` // Use this inside the Attack to lock resource.
+ Status string
// Results contains list of load testing output.
Results []*loadTestingResult
- // AllowLoadTesting if its true, the "Run load testing" will be showed
- // on user interface.
- AllowLoadTesting bool
-
- mtx sync.Mutex
+ // AllowAttack if its true the "Attack" button will be showed on user
+ // interface to allow client to run load testing on this HttpTarget.
+ AllowAttack bool
}
func (ht *HttpTarget) init() {
diff --git a/key_value.go b/key_value.go
index 3814f81..7147118 100644
--- a/key_value.go
+++ b/key_value.go
@@ -5,8 +5,15 @@ import (
"net/url"
)
+//
+// KeyValue is the simplified type for getting and setting HTTP headers and
+// request parameters (either in query or in the parameter body).
+//
type KeyValue map[string]string
+//
+// ToHttpHeader convert the KeyValue to the standard http.Header.
+//
func (kv KeyValue) ToHttpHeader() http.Header {
if kv == nil || len(kv) == 0 {
return nil
@@ -18,6 +25,9 @@ func (kv KeyValue) ToHttpHeader() http.Header {
return headers
}
+//
+// ToUrlValues convert the KeyValue to the standard url.Values.
+//
func (kv KeyValue) ToUrlValues() url.Values {
if kv == nil || len(kv) == 0 {
return nil
diff --git a/load_testing_result.go b/load_testing_result.go
index 4018a95..1c0e0cd 100644
--- a/load_testing_result.go
+++ b/load_testing_result.go
@@ -16,9 +16,10 @@ import (
"sync"
"time"
+ vegeta "github.com/tsenart/vegeta/v12/lib"
+
libbytes "github.com/shuLhan/share/lib/bytes"
"github.com/shuLhan/share/lib/mlog"
- vegeta "github.com/tsenart/vegeta/v12/lib"
)
const (
@@ -39,8 +40,8 @@ type loadTestingResult struct {
HistReport []byte // HistReport the result reported as histogram text.
fullpath string
- req *request
fout *os.File
+ encoder vegeta.Encoder
metrics *vegeta.Metrics
hist *vegeta.Histogram
}
@@ -48,20 +49,19 @@ type loadTestingResult struct {
//
// newLoadTestingResult create new load testing result from request.
//
-func newLoadTestingResult(env *Environment, target *Target, httpTarget *HttpTarget, req *request) (
+func newLoadTestingResult(env *Environment, rr *RunRequest) (
ltr *loadTestingResult, err error,
) {
ltr = &loadTestingResult{
- TargetID: httpTarget.ID,
- req: req,
+ TargetID: rr.HttpTarget.ID,
metrics: &vegeta.Metrics{},
hist: &vegeta.Histogram{},
}
ltr.Name = fmt.Sprintf("%s.%s.%dx%s.%s.bin", ltr.TargetID,
time.Now().Format(outputSuffixDate),
- target.AttackOpts.RatePerSecond, target.AttackOpts.Duration,
- req.Env.ResultsSuffix)
+ rr.Target.Opts.RatePerSecond, rr.Target.Opts.Duration,
+ env.ResultsSuffix)
err = ltr.hist.Buckets.UnmarshalText([]byte(histogramBuckets))
if err != nil {
@@ -75,41 +75,21 @@ func newLoadTestingResult(env *Environment, target *Target, httpTarget *HttpTarg
return nil, fmt.Errorf("newLoadTestingResult: %w", err)
}
+ ltr.encoder = vegeta.NewEncoder(ltr.fout)
+
return ltr, nil
}
-func (ltr *loadTestingResult) init(path string) (err error) {
- ltr.fullpath = filepath.Join(path, ltr.Name)
-
- result, err := ioutil.ReadFile(ltr.fullpath)
- if err != nil {
- return err
- }
-
- dec := vegeta.NewDecoder(bytes.NewReader(result))
-
- ltr.metrics = &vegeta.Metrics{}
- ltr.hist = &vegeta.Histogram{}
-
- err = ltr.hist.Buckets.UnmarshalText([]byte(histogramBuckets))
+func (ltr *loadTestingResult) add(res *vegeta.Result) (err error) {
+ err = ltr.encoder.Encode(res)
if err != nil {
- return err
+ return fmt.Errorf("loadTestingResult.add: %w", err)
}
- for {
- var res vegeta.Result
- err = dec.Decode(&res)
- if err != nil {
- if errors.Is(err, io.EOF) {
- break
- }
- return err
- }
- ltr.metrics.Add(&res)
- ltr.hist.Add(&res)
- }
+ ltr.metrics.Add(res)
+ ltr.hist.Add(res)
- return ltr.finish()
+ return nil
}
func (ltr *loadTestingResult) cancel() {
@@ -181,6 +161,40 @@ func (ltr *loadTestingResult) finish() (err error) {
return nil
}
+func (ltr *loadTestingResult) init(path string) (err error) {
+ ltr.fullpath = filepath.Join(path, ltr.Name)
+
+ result, err := ioutil.ReadFile(ltr.fullpath)
+ if err != nil {
+ return err
+ }
+
+ dec := vegeta.NewDecoder(bytes.NewReader(result))
+
+ ltr.metrics = &vegeta.Metrics{}
+ ltr.hist = &vegeta.Histogram{}
+
+ err = ltr.hist.Buckets.UnmarshalText([]byte(histogramBuckets))
+ if err != nil {
+ return err
+ }
+
+ for {
+ var res vegeta.Result
+ err = dec.Decode(&res)
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return err
+ }
+ ltr.metrics.Add(&res)
+ ltr.hist.Add(&res)
+ }
+
+ return ltr.finish()
+}
+
func (ltr *loadTestingResult) pack() (b []byte, err error) {
ltr.Lock()
b, err = json.Marshal(ltr)
diff --git a/run_request.go b/run_request.go
index 2d06e23..1f72b96 100644
--- a/run_request.go
+++ b/run_request.go
@@ -4,7 +4,52 @@
package trunks
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ vegeta "github.com/tsenart/vegeta/v12/lib"
+)
+
type RunRequest struct {
+ Locker sync.Mutex
Target *Target
HttpTarget *HttpTarget
+ result *loadTestingResult
+}
+
+func (rr *RunRequest) String() string {
+ return fmt.Sprintf("Target:%v HttpTarget:%v\n", rr.Target, rr.HttpTarget)
+}
+
+//
+// merge the request parameter into original target and HTTP target.
+//
+func (rr *RunRequest) merge(env *Environment, origTarget *Target, origHttpTarget *HttpTarget) {
+ if rr.Target.Opts.Duration > 0 &&
+ rr.Target.Opts.Duration <= env.MaxAttackDuration {
+ origTarget.Opts.Duration = rr.Target.Opts.Duration
+ }
+
+ if rr.Target.Opts.RatePerSecond > 0 &&
+ rr.Target.Opts.RatePerSecond <= env.MaxAttackRate {
+ origTarget.Opts.RatePerSecond = rr.Target.Opts.RatePerSecond
+ origTarget.Opts.ratePerSecond = vegeta.Rate{
+ Freq: rr.Target.Opts.RatePerSecond,
+ Per: time.Second,
+ }
+ }
+
+ if rr.Target.Opts.Timeout > 0 &&
+ rr.Target.Opts.Timeout <= DefaultAttackTimeout {
+ origTarget.Opts.Timeout = rr.Target.Opts.Timeout
+ }
+
+ origTarget.Vars = rr.Target.Vars
+ rr.Target = origTarget
+
+ origHttpTarget.Headers = rr.HttpTarget.Headers
+ origHttpTarget.Params = rr.HttpTarget.Params
+ rr.HttpTarget = origHttpTarget
}
diff --git a/target.go b/target.go
index b01eb52..65a7a96 100644
--- a/target.go
+++ b/target.go
@@ -11,13 +11,12 @@ import (
)
//
-// Target contains group of HTTP and/or WebSocket endpoints that can be tested
-// by Trunks.
+// Target contains group of HttpTarget that can be tested by Trunks.
//
type Target struct {
ID string
Name string
- AttackOpts *AttackOptions
+ Opts *AttackOptions
Vars map[string]string
HttpTargets []*HttpTarget
@@ -32,11 +31,11 @@ func (target *Target) init() (err error) {
target.ID = generateID(target.Name)
- if target.AttackOpts == nil {
- target.AttackOpts = &AttackOptions{}
+ if target.Opts == nil {
+ target.Opts = &AttackOptions{}
}
- err = target.AttackOpts.init()
+ err = target.Opts.init()
if err != nil {
return err
}
diff --git a/trunks.go b/trunks.go
index 9b47fb6..eeb2d04 100644
--- a/trunks.go
+++ b/trunks.go
@@ -16,8 +16,11 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "sort"
"time"
+ vegeta "github.com/tsenart/vegeta/v12/lib"
+
"github.com/shuLhan/share/lib/debug"
libhttp "github.com/shuLhan/share/lib/http"
"github.com/shuLhan/share/lib/memfs"
@@ -43,8 +46,8 @@ const (
)
//
-// Trunks is the HTTP server with web user interface for running HTTP test and
-// load testing.
+// Trunks is the HTTP server with web user interface and APIs for running and
+// load testing the registered HTTP endpoints.
//
type Trunks struct {
*libhttp.Server
@@ -52,8 +55,7 @@ type Trunks struct {
Env *Environment
targets []*Target
- ltrq chan *loadTestingResult
- finishq chan *loadTestingResult
+ attackq chan *RunRequest
cancelq chan bool
}
@@ -67,7 +69,9 @@ func New(env *Environment) (trunks *Trunks, err error) {
}
trunks = &Trunks{
- Env: env,
+ Env: env,
+ attackq: make(chan *RunRequest, 1),
+ cancelq: make(chan bool, 1),
}
httpdOpts := &libhttp.ServerOptions{
@@ -112,6 +116,9 @@ func (trunks *Trunks) RegisterTarget(target *Target) (err error) {
// load testing registered Targets.
//
func (trunks *Trunks) Start() (err error) {
+ mlog.Outf("Starting attack worker...\n")
+ go trunks.workerAttackQueue()
+
mlog.Outf("starting HTTP server at %s\n", trunks.Env.ListenAddress)
return trunks.Server.Start()
}
@@ -135,7 +142,7 @@ func (trunks *Trunks) Stop() {
func (trunks *Trunks) isLoadTesting() (b bool) {
trunks.Env.mtx.Lock()
- if trunks.Env.LoadTestingRunning != nil {
+ if trunks.Env.AttackRunning != nil {
b = true
}
trunks.Env.mtx.Unlock()
@@ -231,8 +238,60 @@ func (trunks *Trunks) apiEnvironmentGet(epr *libhttp.EndpointRequest) (resbody [
return json.Marshal(&res)
}
+//
+// apiTargetAttack run the load testing on HTTP endpoint with target and
+// options defined in request.
+//
func (trunks *Trunks) apiTargetAttack(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
- return resbody, nil
+ if trunks.Env.isAttackRunning() {
+ return nil, errAttackConflict(trunks.Env.getRunningAttack())
+ }
+
+ logp := "apiTargetAttack"
+ req := &RunRequest{}
+
+ err = json.Unmarshal(epr.RequestBody, req)
+ if err != nil {
+ return nil, errInternal(err)
+ }
+ if req.Target == nil {
+ return nil, errInvalidTarget("")
+ }
+
+ origTarget := trunks.getTargetByID(req.Target.ID)
+ if origTarget == nil {
+ return nil, errInvalidTarget(req.Target.ID)
+ }
+
+ origHttpTarget := origTarget.getHttpTargetByID(req.HttpTarget.ID)
+ if origTarget == nil {
+ return nil, errInvalidHttpTarget(req.HttpTarget.ID)
+ }
+
+ if !origHttpTarget.AllowAttack {
+ return nil, errAttackNotAllowed()
+ }
+
+ req.merge(trunks.Env, origTarget, origHttpTarget)
+
+ req.result, err = newLoadTestingResult(trunks.Env, req)
+ if err != nil {
+ return nil, err
+ }
+
+ trunks.attackq <- req
+
+ msg := fmt.Sprintf("attacking %s/%s with %d RPS for %d seconds",
+ req.Target.Opts.BaseUrl, req.HttpTarget.Path,
+ req.Target.Opts.RatePerSecond, req.Target.Opts.Duration)
+
+ mlog.Outf("%s: %s\n", logp, msg)
+
+ res := libhttp.EndpointResponse{}
+ res.Code = http.StatusOK
+ res.Message = msg
+
+ return json.Marshal(res)
}
func (trunks *Trunks) apiTargetAttackCancel(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
@@ -289,3 +348,65 @@ func (trunks *Trunks) getTargetByID(id string) *Target {
}
return nil
}
+
+func (trunks *Trunks) workerAttackQueue() (err error) {
+ logp := "workerAttackQueue"
+
+ for rr := range trunks.attackq {
+ rr.HttpTarget.PreAttack(rr)
+
+ isCancelled := false
+ attacker := vegeta.NewAttacker(
+ vegeta.Timeout(rr.Target.Opts.Timeout),
+ )
+
+ for res := range attacker.Attack(
+ rr.HttpTarget.Attack(rr),
+ rr.Target.Opts.ratePerSecond,
+ rr.Target.Opts.Duration,
+ rr.HttpTarget.ID,
+ ) {
+ err = rr.result.add(res)
+ if err != nil {
+ attacker.Stop()
+ rr.result.cancel()
+ break
+ }
+
+ select {
+ case <-trunks.cancelq:
+ isCancelled = true
+ default:
+ }
+ if isCancelled {
+ break
+ }
+ }
+
+ if err != nil || isCancelled {
+ attacker.Stop()
+ rr.result.cancel()
+
+ if err != nil {
+ mlog.Errf("%s: %s fail: %w.\n", logp, rr.result.Name, err)
+ } else {
+ mlog.Outf("%s: %s canceled.\n", logp, rr.result.Name)
+ trunks.cancelq <- true
+ }
+ } else {
+ err := rr.result.finish()
+ if err != nil {
+ mlog.Errf("%s %s: %s\n", logp, rr.result.TargetID, err)
+ }
+
+ rr.HttpTarget.Results = append(rr.HttpTarget.Results, rr.result)
+
+ sort.Slice(rr.HttpTarget.Results, func(x, y int) bool {
+ return rr.HttpTarget.Results[x].Name > rr.HttpTarget.Results[y].Name
+ })
+
+ mlog.Outf("%s: %s finished.\n", logp, rr.result.Name)
+ }
+ }
+ return nil
+}