diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 8e24202a..21b4f74a 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -14,7 +14,7 @@ pipeline { stage('Performance Test') { steps { lock('multi_branch_server_benchmark') { - sh 'GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' + sh 'set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' } } } @@ -29,7 +29,7 @@ pipeline { } steps { lock('multi_branch_server_benchmark') { - sh 'git fetch origin develop:develop || git checkout develop && git pull && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' + sh 'git fetch origin develop:develop || git checkout develop && git pull && set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' sh "git checkout ${BRANCH_NAME}" sh "benchstat -alpha 1.01 --sort delta gobench_develop.txt gobench_branch.txt | tee gobench_branch_result.txt" sh "benchstat -alpha 1.01 --sort delta --html gobench_develop.txt gobench_branch.txt > 00_gobench_result.html && echo ${BUILD_URL}artifact/00_gobench_result.html" diff --git a/README.md b/README.md index 8381bc59..1dbb67e1 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ The features you can use by config file; - Custom load type creation - Payload from a file - Multipart/form-data payload -- Extra connection configuration, like *keep-alive* enable/disable logic +- Extra connection configuration - HTTP2 support @@ -274,6 +274,12 @@ There is an example config file at [config_examples/config.json](/config_example - `output` *optional* This is the equivalent of the `-o` flag. +- `engine_mode` *optional* + Can be one of `distinct-user`, `repeated-user`, or default mode `ddosify`. + - `distinct-user` mode simulates a new user for every iteration. + - `repeated-user` mode can use pre-used user in subsequent iterations. + - `ddosify` mode is default mode of the engine. In this mode engine runs in its max capacity, and does not show user simulation behaviour. + - `env` *optional* Scenario-scoped global variables. Note that dynamic variables changes every iteration. ```json @@ -282,6 +288,7 @@ There is an example config file at [config_examples/config.json](/config_example "randomCountry" : "{{_randomCountry}}" } ``` + - `data` *optional* Config for loading test data from a CSV file. [CSV data](https://github.com/ddosify/ddosify/tree/master/config/config_testdata/test.csv) used in below config. @@ -494,7 +501,6 @@ There is an example config file at [config_examples/config.json](/config_example ```json "others": { - "keep-alive": true, // Default true "disable-compression": false, // Default true "h2": true, // Enables HTTP/2. Default false. "disable-redirect": true // Default false diff --git a/config/config_testdata/benchmark/config_correlation_load_1.json b/config/config_testdata/benchmark/config_correlation_load_1.json index 375adf37..9a969a0b 100644 --- a/config/config_testdata/benchmark/config_correlation_load_1.json +++ b/config/config_testdata/benchmark/config_correlation_load_1.json @@ -1,5 +1,6 @@ { "iteration_count": 100, + "engine_mode": "ddosify", "load_type": "waved", "duration": 10, "steps": [ @@ -10,7 +11,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -36,7 +36,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -55,7 +54,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_2.json b/config/config_testdata/benchmark/config_correlation_load_2.json index 785776dd..f62bc132 100644 --- a/config/config_testdata/benchmark/config_correlation_load_2.json +++ b/config/config_testdata/benchmark/config_correlation_load_2.json @@ -1,6 +1,7 @@ { "iteration_count": 1000, "load_type": "waved", + "engine_mode": "ddosify", "duration": 10, "steps": [ { @@ -10,7 +11,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -36,7 +36,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -55,7 +54,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_3.json b/config/config_testdata/benchmark/config_correlation_load_3.json index c4be1283..f4d78694 100644 --- a/config/config_testdata/benchmark/config_correlation_load_3.json +++ b/config/config_testdata/benchmark/config_correlation_load_3.json @@ -1,6 +1,7 @@ { "iteration_count": 5000, "load_type": "waved", + "engine_mode": "ddosify", "duration": 10, "steps": [ { @@ -10,7 +11,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -36,7 +36,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -55,7 +54,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_4.json b/config/config_testdata/benchmark/config_correlation_load_4.json index ca1fbb0f..dd2908c7 100644 --- a/config/config_testdata/benchmark/config_correlation_load_4.json +++ b/config/config_testdata/benchmark/config_correlation_load_4.json @@ -1,6 +1,8 @@ { "iteration_count": 10000, "load_type": "waved", + "engine_mode": "ddosify", + "duration": 10, "steps": [ { @@ -10,7 +12,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -36,7 +37,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -55,7 +55,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_5.json b/config/config_testdata/benchmark/config_correlation_load_5.json index c38f24d2..f051bab8 100644 --- a/config/config_testdata/benchmark/config_correlation_load_5.json +++ b/config/config_testdata/benchmark/config_correlation_load_5.json @@ -1,6 +1,8 @@ { "iteration_count": 20000, "load_type": "waved", + "engine_mode": "ddosify", + "duration": 10, "steps": [ { @@ -10,7 +12,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -36,7 +37,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -55,7 +55,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/config.json b/config/config_testdata/config.json index e223fb95..6ace2b82 100644 --- a/config/config_testdata/config.json +++ b/config/config_testdata/config.json @@ -12,7 +12,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_data_csv.json b/config/config_testdata/config_data_csv.json index 13b6881d..24d5bbf8 100644 --- a/config/config_testdata/config_data_csv.json +++ b/config/config_testdata/config_data_csv.json @@ -10,7 +10,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/config_debug_false.json b/config/config_testdata/config_debug_false.json index 92e18baa..874d8d7a 100644 --- a/config/config_testdata/config_debug_false.json +++ b/config/config_testdata/config_debug_false.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_debug_mode.json b/config/config_testdata/config_debug_mode.json index 1e661c64..08ed4adc 100644 --- a/config/config_testdata/config_debug_mode.json +++ b/config/config_testdata/config_debug_mode.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_env_vars.json b/config/config_testdata/config_env_vars.json index 93c5a7ea..6b670b6b 100644 --- a/config/config_testdata/config_env_vars.json +++ b/config/config_testdata/config_env_vars.json @@ -12,7 +12,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true }, "capture_env": { "ENV_VAR1" :{"json_path":""}, diff --git a/config/config_testdata/config_incorrect.json b/config/config_testdata/config_incorrect.json index f31b65d8..c1d17c24 100644 --- a/config/config_testdata/config_incorrect.json +++ b/config/config_testdata/config_incorrect.json @@ -19,7 +19,6 @@ "payload": "body yt kanl adnlandlandaln", "timeout": 1, "others": { - "keep-alive": true } }, { @@ -33,7 +32,6 @@ "payload_file": "config_examples/payload.txt", "timeout": 1, "others": { - "keep-alive": false } }, ], diff --git a/config/config_testdata/config_iteration_count.json b/config/config_testdata/config_iteration_count.json index 54dc3612..e33a75a3 100644 --- a/config/config_testdata/config_iteration_count.json +++ b/config/config_testdata/config_iteration_count.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_iteration_count_over_req_count.json b/config/config_testdata/config_iteration_count_over_req_count.json index bb6470f5..6e71eea8 100644 --- a/config/config_testdata/config_iteration_count_over_req_count.json +++ b/config/config_testdata/config_iteration_count_over_req_count.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/json.go b/config/json.go index 9af0a03d..8caf9936 100644 --- a/config/json.go +++ b/config/json.go @@ -156,14 +156,16 @@ type JsonReader struct { Data map[string]CsvConf `json:"data"` Debug bool `json:"debug"` SamplingRate *int `json:"sampling_rate"` + EngineMode string `json:"engine_mode"` } func (j *JsonReader) UnmarshalJSON(data []byte) error { type jsonReaderAlias JsonReader defaultFields := &jsonReaderAlias{ - LoadType: types.DefaultLoadType, - Duration: types.DefaultDuration, - Output: types.DefaultOutputType, + LoadType: types.DefaultLoadType, + Duration: types.DefaultDuration, + Output: types.DefaultOutputType, + EngineMode: types.EngineModeDdosify, } err := json.Unmarshal(data, defaultFields) @@ -274,6 +276,7 @@ func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { ReportDestination: j.Output, Debug: j.Debug, SamplingRate: samplingRate, + EngineMode: j.EngineMode, TestDataConf: testDataConf, } return diff --git a/config/json_test.go b/config/json_test.go index f30e5d58..30718b59 100644 --- a/config/json_test.go +++ b/config/json_test.go @@ -56,6 +56,7 @@ func TestCreateHammerDefaultValues(t *testing.T) { Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -89,9 +90,7 @@ func TestCreateHammer(t *testing.T) { Timeout: 3, Sleep: "1000", Payload: "payload str", - Custom: map[string]interface{}{ - "keep-alive": true, - }, + Custom: map[string]interface{}{}, }, { ID: 2, @@ -112,6 +111,7 @@ func TestCreateHammer(t *testing.T) { Addr: addr, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -145,9 +145,7 @@ func TestCreateHammerWithIterationCountInsteadOfReqCount(t *testing.T) { Timeout: 3, Sleep: "1000", Payload: "payload str", - Custom: map[string]interface{}{ - "keep-alive": true, - }, + Custom: map[string]interface{}{}, }, { ID: 2, @@ -168,6 +166,7 @@ func TestCreateHammerWithIterationCountInsteadOfReqCount(t *testing.T) { Addr: addr, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -203,9 +202,7 @@ func TestCreateHammerWithIterationCountOverridesReqCount(t *testing.T) { Timeout: 3, Sleep: "1000", Payload: "payload str", - Custom: map[string]interface{}{ - "keep-alive": true, - }, + Custom: map[string]interface{}{}, }, { ID: 2, @@ -227,6 +224,7 @@ func TestCreateHammerWithIterationCountOverridesReqCount(t *testing.T) { Addr: addr, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -263,6 +261,7 @@ func TestCreateHammerManualLoad(t *testing.T) { Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -299,6 +298,7 @@ func TestCreateHammerManualLoadOverrideOthers(t *testing.T) { Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -521,6 +521,7 @@ func TestCreateHammerTLSWithOnlyCertPath(t *testing.T) { Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -567,6 +568,7 @@ func TestCreateHammerTLSWithOnlyKeyPath(t *testing.T) { Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } @@ -604,6 +606,7 @@ func TestCreateHammerTLSWithWithEmptyPath(t *testing.T) { Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), } diff --git a/config_examples/config.json b/config_examples/config.json index a642db5b..01eafeb5 100644 --- a/config_examples/config.json +++ b/config_examples/config.json @@ -4,6 +4,7 @@ "iteration_count": 30, "debug" : false, // use this field for debugging, see verbose result "load_type": "linear", + "engine_mode": "distinct-user", // could be one of "distinct-user","repeated-user", or default mode "ddosify" "duration": 5, "manual_load": [ {"duration": 5, "count": 5}, @@ -54,7 +55,6 @@ "password": "12345" }, "others": { - "keep-alive": true, "disable-compression": false, "h2": true, "disable-redirect": true diff --git a/core/engine.go b/core/engine.go index 4061fce8..c17c5bed 100644 --- a/core/engine.go +++ b/core/engine.go @@ -92,10 +92,10 @@ func NewEngine(ctx context.Context, h types.Hammer) (e *engine, err error) { } func (e *engine) Init() (err error) { + e.initReqCountArr() if err = e.proxyService.Init(e.hammer.Proxy); err != nil { return } - // read test data readData, err := readTestData(e.hammer.TestDataConf) if err != nil { @@ -103,7 +103,21 @@ func (e *engine) Init() (err error) { } e.hammer.Scenario.Data = readData - if err = e.scenarioService.Init(e.ctx, e.hammer.Scenario, e.proxyService.GetAll(), e.hammer.Debug); err != nil { + if err = e.scenarioService.Init(e.ctx, e.hammer.Scenario, e.proxyService.GetAll(), scenario.ScenarioOpts{ + Debug: e.hammer.Debug, + IterationCount: e.hammer.IterationCount, + MaxConcurrentIterCount: e.getMaxConcurrentIterCount(), + EngineMode: e.hammer.EngineMode, + }); err != nil { + return + } + + if err = e.scenarioService.Init(e.ctx, e.hammer.Scenario, e.proxyService.GetAll(), scenario.ScenarioOpts{ + Debug: e.hammer.Debug, + IterationCount: e.hammer.IterationCount, + MaxConcurrentIterCount: e.getMaxConcurrentIterCount(), + EngineMode: e.hammer.EngineMode, + }); err != nil { return } @@ -111,7 +125,6 @@ func (e *engine) Init() (err error) { return } - e.initReqCountArr() return } @@ -192,6 +205,16 @@ func (e *engine) stop() { e.scenarioService.Done() } +func (e *engine) getMaxConcurrentIterCount() int { + max := 0 + for _, v := range e.reqCountArr { + if v > max { + max = v + } + } + return max +} + func (e *engine) initReqCountArr() { if e.hammer.Debug { e.reqCountArr = []int{1} diff --git a/core/engine_test.go b/core/engine_test.go index 634d7643..9830f7db 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -1772,6 +1772,297 @@ func TestTLSMutualAuthButServerAndClientHasDifferentCerts(t *testing.T) { } } +func TestEngineModeUserKeepAlive(t *testing.T) { + t.Parallel() + // For DistinctUser and RepeatedUser modes + + // Test server + clientAddress1 := []string{} + clientAddress2 := []string{} + var m1 sync.Mutex + var m2 sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m1.Lock() + defer m1.Unlock() + + clientAddress1 = append(clientAddress1, r.RemoteAddr) // network address that sent the request + } + + secondReqHandler := func(w http.ResponseWriter, r *http.Request) { + m2.Lock() + defer m2.Unlock() + + clientAddress2 = append(clientAddress2, r.RemoteAddr) // network address that sent the request + } + + pathFirst := "/first" + pathSecond := "/second" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + mux.HandleFunc(pathSecond, secondReqHandler) + + host := httptest.NewServer(mux) + defer host.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 2 + h.Scenario.Steps = make([]types.ScenarioStep, 2) + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host.URL + pathFirst, + } + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host.URL + pathSecond, + } + + // Act + h.EngineMode = types.EngineModeRepeatedUser // could have been DistinctUser also + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) + } + + e.Start() + + // same host + + // check first iter + if clientAddress1[0] != clientAddress2[0] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, same hosts connection should be same throughout iteration") + } + // check second iter + if clientAddress1[1] != clientAddress2[1] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, same hosts connection should be same throughout iteration") + } + +} + +func TestEngineModeUserKeepAliveDifferentHosts(t *testing.T) { + t.Parallel() + // For DistinctUser and RepeatedUser modes + + // Test server + clientAddress := make(map[string]struct{}) + var m sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m.Lock() + defer m.Unlock() + clientAddress[r.RemoteAddr] = struct{}{} // network address that sent the request + } + + pathFirst := "/first" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + + host1 := httptest.NewServer(mux) + host2 := httptest.NewServer(mux) + + defer host1.Close() + defer host2.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 1 + h.Scenario.Steps = make([]types.ScenarioStep, 4) + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[2] = types.ScenarioStep{ + ID: 3, + Method: "GET", + URL: host2.URL + pathFirst, + } + h.Scenario.Steps[3] = types.ScenarioStep{ + ID: 4, + Method: "GET", + URL: host2.URL + pathFirst, + } + + // Act + h.EngineMode = types.EngineModeDistinctUser // could have been RepeatedUser also + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + e.Start() + + // one iteration, two hosts, two connections expected + if len(clientAddress) != 2 { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) + } +} + +func TestEngineModeUserKeepAlive_StepsKeepAliveFalse(t *testing.T) { + t.Parallel() + // For DistinctUser and RepeatedUser modes + // Test server + clientAddress := make(map[string]struct{}) + var m sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m.Lock() + defer m.Unlock() + clientAddress[r.RemoteAddr] = struct{}{} // network address that sent the request + } + + pathFirst := "/first" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + + host1 := httptest.NewServer(mux) + + defer host1.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 1 + h.Scenario.Steps = make([]types.ScenarioStep, 4) + // connection opened by 1 will not be reused + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host1.URL + pathFirst, + Headers: map[string]string{"Connection": "close"}, + } + // below will use the connection opened by 2 + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[2] = types.ScenarioStep{ + ID: 3, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[3] = types.ScenarioStep{ + ID: 4, + Method: "GET", + URL: host1.URL + pathFirst, + } + + // Act + h.EngineMode = types.EngineModeDistinctUser + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + e.Start() + + // one iteration, one host, 4 steps, one's keep-alive is false (Connection: close) + if len(clientAddress) != 2 { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) + } + +} + +func TestEngineModeDdosifyKeepAlive(t *testing.T) { + t.Parallel() + + // Test server + clientAddress1 := []string{} + clientAddress2 := []string{} + var m1 sync.Mutex + var m2 sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m1.Lock() + defer m1.Unlock() + + clientAddress1 = append(clientAddress1, r.RemoteAddr) // network address that sent the request + } + + secondReqHandler := func(w http.ResponseWriter, r *http.Request) { + m2.Lock() + defer m2.Unlock() + + clientAddress2 = append(clientAddress2, r.RemoteAddr) // network address that sent the request + } + + pathFirst := "/first" + pathSecond := "/second" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + mux.HandleFunc(pathSecond, secondReqHandler) + + host := httptest.NewServer(mux) + defer host.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 2 + h.Scenario.Steps = make([]types.ScenarioStep, 2) + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host.URL + pathFirst, + } + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host.URL + pathSecond, + } + + // Act + h.EngineMode = types.EngineModeDdosify + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeDdosifyKeepAlive error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeDdosifyKeepAlive error occurred %v", err) + } + + e.Start() + + // same host + // in ddosify mode every step has its own client, therefore connections should be different + // check first iter + if clientAddress1[0] == clientAddress2[0] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, ") + } + // check second iter + if clientAddress1[1] == clientAddress2[1] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, ") + } + +} func createCertPairFiles(cert string, certKey string) (*os.File, *os.File, error) { certFile, err := os.CreateTemp("", ".pem") if err != nil { diff --git a/core/scenario/client_pool.go b/core/scenario/client_pool.go new file mode 100644 index 00000000..16ba778c --- /dev/null +++ b/core/scenario/client_pool.go @@ -0,0 +1,85 @@ +package scenario + +import ( + "errors" + "net/http" +) + +type clientPool struct { + // storage for our http.Clients + clients chan *http.Client + factory Factory +} + +// Factory is a function to create new connections. +type Factory func() *http.Client + +// NewClientPool returns a new pool based on buffered channels with an initial +// capacity and maximum capacity. Factory is used when initial capacity is +// greater than zero to fill the pool. A zero initialCap doesn't fill the Pool +// until a new Get() is called. During a Get(), If there is no new client +// available in the pool, a new client will be created via the Factory() +// method. +func NewClientPool(initialCap, maxCap int, factory Factory) (*clientPool, error) { + if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { + return nil, errors.New("invalid capacity settings") + } + + pool := &clientPool{ + clients: make(chan *http.Client, maxCap), + factory: factory, + } + + // create initial clients, if something goes wrong, + // just close the pool error out. + for i := 0; i < initialCap; i++ { + client := pool.factory() + pool.clients <- client + } + + return pool, nil +} + +func (c *clientPool) Get() *http.Client { + var client *http.Client + select { + case client = <-c.clients: + default: + client = c.factory() + } + return client +} + +func (c *clientPool) Put(client *http.Client) error { + if client == nil { + return errors.New("client is nil. rejecting") + } + + if c.clients == nil { + // pool is closed, close passed client + client.CloseIdleConnections() + return nil + } + + // put the resource back into the pool. If the pool is full, this will + // block and the default case will be executed. + select { + case c.clients <- client: + return nil + default: + // pool is full, close passed client + client.CloseIdleConnections() + return nil + } +} + +func (c *clientPool) Len() int { + return len(c.clients) +} + +func (c *clientPool) Done() { + close(c.clients) + for c := range c.clients { + c.CloseIdleConnections() + } +} diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index d5bbe239..57fff9e8 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -22,6 +22,7 @@ package requester import ( "context" + "net/http" "net/url" "go.ddosify.com/ddosify/core/scenario/scripting/injection" @@ -31,11 +32,15 @@ import ( // Requester is the interface that abstracts different protocols' request sending implementations. // Protocol field in the types.ScenarioStep determines which requester implementation to use. type Requester interface { - Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error - Send(envs map[string]interface{}) *types.ScenarioStepResult + Type() string Done() } +type HttpRequesterI interface { + Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error + Send(client *http.Client, envs map[string]interface{}) *types.ScenarioStepResult // should use its own client if client is nil +} + // NewRequester is the factory method of the Requester. func NewRequester(s types.ScenarioStep) (requester Requester, err error) { requester = &HttpRequester{} // we have only HttpRequester type for now, add check for rpc in future diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 1d229818..5ed359a9 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -73,11 +73,10 @@ func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd h.dynamicRgx = regexp.MustCompile(regex.DynamicVariableRegex) h.envRgx = regexp.MustCompile(regex.EnvironmentVariableRegex) - // TlsConfig - tlsConfig := h.initTLSConfig() - // Transport segment - tr := h.initTransport(tlsConfig) + tr := h.initTransport() + tr.MaxIdleConnsPerHost = 60000 + tr.MaxIdleConns = 0 // http client h.client = &http.Client{Transport: tr, Timeout: time.Duration(h.packet.Timeout) * time.Second} @@ -165,10 +164,11 @@ func (h *HttpRequester) Done() { // let us reuse the connections when keep-alive enabled(default) // When the Job is finished, we have to Close idle connections to prevent sockets to lock in at the TIME_WAIT state. // Otherwise, the next job can't use these sockets because they are reserved for the current target host. + h.client.CloseIdleConnections() } -func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioStepResult) { +func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { var statusCode int var contentLength int64 var requestErr types.RequestError @@ -188,8 +188,24 @@ func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioSt usableVars[k] = v } + if client == nil { + // engine mode is 'ddosify' + // if passed client is nil , use requesters client that is dedicated to one step, thereby one transport + client = h.client + } else { + // engine mode is 'distinct-user' or 'repeated-user' + // passed client is used for multiple steps throughout an iteration, update transport + if client.Transport == nil { + client.Transport = h.initTransport() + client.Transport.(*http.Transport).MaxConnsPerHost = 1 // use same connection per host throughout an iteration + } else { + h.updateTransport(client.Transport.(*http.Transport)) + } + } + durations := &duration{} - trace := newTrace(durations, h.proxyAddr) + headersAddedByClient := make(map[string][]string) + trace := newTrace(durations, h.proxyAddr, headersAddedByClient) httpReq, err := h.prepareReq(usableVars, trace) if err != nil { // could not prepare req @@ -209,7 +225,7 @@ func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioSt httpReq.Body = io.NopCloser(bytes.NewReader(copiedReqBody.Bytes())) // Action - httpRes, err := h.client.Do(httpReq) + httpRes, err := client.Do(httpReq) if err != nil { requestErr = fetchErrType(err) failedCaptures = h.captureEnvironmentVariables(nil, nil, extractedVars) @@ -276,7 +292,7 @@ func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioSt Url: httpReq.URL.String(), Method: httpReq.Method, - ReqHeaders: httpReq.Header, + ReqHeaders: concatHeaders(httpReq.Header, headersAddedByClient), ReqBody: copiedReqBody.Bytes(), RespHeaders: respHeaders, RespBody: respBody, @@ -319,6 +335,20 @@ func concatEnvs(envs1, envs2 map[string]interface{}) map[string]interface{} { return total } +func concatHeaders(envs1, envs2 map[string][]string) map[string][]string { + total := make(map[string][]string) + + for k, v := range envs1 { + total[k] = v + } + + for k, v := range envs2 { + total[k] = v + } + + return total +} + func (h *HttpRequester) prepareReq(envs map[string]interface{}, trace *httptrace.ClientTrace) (*http.Request, error) { re := regexp.MustCompile(regex.DynamicVariableRegex) httpReq := h.request.Clone(h.ctx) @@ -448,17 +478,15 @@ func fetchErrType(err error) types.RequestError { return requestErr } -func (h *HttpRequester) initTransport(tlsConfig *tls.Config) *http.Transport { +func (h *HttpRequester) initTransport() *http.Transport { tr := &http.Transport{ - TLSClientConfig: tlsConfig, - Proxy: http.ProxyURL(h.proxyAddr), - MaxIdleConnsPerHost: 60000, - MaxIdleConns: 0, + TLSClientConfig: h.initTLSConfig(), + Proxy: http.ProxyURL(h.proxyAddr), } tr.DisableKeepAlives = false - if val, ok := h.packet.Custom["keep-alive"]; ok { - tr.DisableKeepAlives = !val.(bool) + if h.packet.Headers["Connection"] == "close" { + tr.DisableKeepAlives = true } if val, ok := h.packet.Custom["disable-compression"]; ok { tr.DisableCompression = val.(bool) @@ -472,6 +500,25 @@ func (h *HttpRequester) initTransport(tlsConfig *tls.Config) *http.Transport { return tr } +func (h *HttpRequester) updateTransport(tr *http.Transport) { + tr.TLSClientConfig = h.initTLSConfig() + tr.Proxy = http.ProxyURL(h.proxyAddr) + + tr.DisableKeepAlives = false + if h.packet.Headers["Connection"] == "close" { + tr.DisableKeepAlives = true + } + if val, ok := h.packet.Custom["disable-compression"]; ok { + tr.DisableCompression = val.(bool) + } + if val, ok := h.packet.Custom["h2"]; ok { + val := val.(bool) + if val { + http2.ConfigureTransport(tr) + } + } +} + func (h *HttpRequester) initTLSConfig() *tls.Config { tlsConfig := &tls.Config{ InsecureSkipVerify: true, @@ -520,13 +567,17 @@ func (h *HttpRequester) initRequestInstance() (err error) { // If keep-alive is false, prevent the reuse of the previous TCP connection at the request layer also. h.request.Close = false - if val, ok := h.packet.Custom["keep-alive"]; ok { - h.request.Close = !val.(bool) + if h.packet.Headers["Connection"] == "close" { + h.request.Close = true } return } -func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { +func (h *HttpRequester) Type() string { + return "HTTP" +} + +func newTrace(duration *duration, proxyAddr *url.URL, headersByClient map[string][]string) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart, serverProcessStart time.Time // According to the doc in the trace.go; @@ -613,6 +664,9 @@ func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { duration.setResStartTime(time.Now()) m.Unlock() }, + WroteHeaderField: func(key string, value []string) { + headersByClient[key] = value + }, } } diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index 9cd7d0a5..b661d6bc 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -90,9 +90,9 @@ func TestInitClient(t *testing.T) { Method: http.MethodGet, URL: "https://test.com", Timeout: types.DefaultTimeout, + Headers: map[string]string{"Connection": "close"}, Custom: map[string]interface{}{ "disable-redirect": true, - "keep-alive": false, "disable-compression": true, "hostname": "dummy.com", }, @@ -279,11 +279,9 @@ func TestInitRequest(t *testing.T) { Password: "123", }, Headers: map[string]string{ - "Header1": "Value1", - "Header2": "Value2", - }, - Custom: map[string]interface{}{ - "keep-alive": false, + "Header1": "Value1", + "Header2": "Value2", + "Connection": "close", }, } expectedWithoutKeepAlive, _ := http.NewRequest(sWithoutKeepAlive.Method, @@ -292,6 +290,7 @@ func TestInitRequest(t *testing.T) { expectedWithoutKeepAlive.Header = make(http.Header) expectedWithoutKeepAlive.Header.Set("Header1", "Value1") expectedWithoutKeepAlive.Header.Set("Header2", "Value2") + expectedWithoutKeepAlive.Header.Set("Connection", "close") expectedWithoutKeepAlive.SetBasicAuth(sWithoutKeepAlive.Auth.Username, sWithoutKeepAlive.Auth.Password) // Sub Tests @@ -367,7 +366,7 @@ func TestSendOnDebugModePopulatesDebugInfo(t *testing.T) { var proxy *url.URL _ = h.Init(ctx, s, proxy, debug, nil) envs := map[string]interface{}{} - res := h.Send(envs) + res := h.Send(http.DefaultClient, envs) if expectedMethod != res.Method { t.Errorf("Method Expected %#v, Found: \n%#v", expectedMethod, res.Method) @@ -379,11 +378,14 @@ func TestSendOnDebugModePopulatesDebugInfo(t *testing.T) { t.Errorf("RequestBody Expected %#v, Found: \n%#v", expectedRequestBody, res.ReqBody) } - if !reflect.DeepEqual(expectedRequestHeaders, res.ReqHeaders) { - t.Errorf("RequestHeaders Expected %#v, Found: \n%#v", expectedRequestHeaders, - res.ReqHeaders) - } + // stepResult has default request headers added by go client + for expKey, expVal := range expectedRequestHeaders { + if !reflect.DeepEqual(expVal, res.ReqHeaders.Values(expKey)) { + t.Errorf("RequestHeaders Expected %#v, Found: \n%#v", expectedRequestHeaders, + res.ReqHeaders) + } + } } t.Run("populate-debug-info", tf) } @@ -427,7 +429,7 @@ func TestCaptureEnvShouldSetEmptyStringWhenReqFails(t *testing.T) { var proxy *url.URL _ = h.Init(ctx, test.scenarioStep, proxy, debug, nil) envs := map[string]interface{}{} - res := h.Send(envs) + res := h.Send(http.DefaultClient, envs) if !reflect.DeepEqual(res.ExtractedEnvs, test.expectedExtractedEnvs) { t.Errorf("Extracted env should be set empty string on req failure") @@ -466,7 +468,7 @@ func TestAssertions(t *testing.T) { h := &HttpRequester{} h.Init(ctx, s, nil, false, nil) - res := h.Send(map[string]interface{}{}) + res := h.Send(http.DefaultClient, map[string]interface{}{}) if !strings.EqualFold(res.FailedAssertions[0].Rule, rule1) { t.Errorf("rule expected %s, got %s", rule1, res.FailedAssertions[0].Rule) diff --git a/core/scenario/service.go b/core/scenario/service.go index e40e53a3..f26dfff9 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -22,7 +22,9 @@ package scenario import ( "context" + "fmt" "math/rand" + "net/http" "net/url" "regexp" "strconv" @@ -44,13 +46,17 @@ type ScenarioService struct { // Each scenarioItem has a requester clients map[*url.URL][]scenarioItemRequester + cPool *clientPool + scenario types.Scenario ctx context.Context clientMutex sync.Mutex debug bool - ei *injection.EnvironmentInjector - iterIndex int64 + engineMode string + + ei *injection.EnvironmentInjector + iterIndex int64 } // NewScenarioService is the constructor of the ScenarioService. @@ -58,13 +64,20 @@ func NewScenarioService() *ScenarioService { return &ScenarioService{} } +type ScenarioOpts struct { + Debug bool + IterationCount int + MaxConcurrentIterCount int + EngineMode string +} + // Init initializes the ScenarioService.clients with the given types.Scenario and proxies. // Passes the given ctx to the underlying requestor so we are able to control the life of each request. func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, - proxies []*url.URL, debug bool) (err error) { + proxies []*url.URL, opts ScenarioOpts) (err error) { s.scenario = scenario s.ctx = ctx - s.debug = debug + s.debug = opts.Debug s.clients = make(map[*url.URL][]scenarioItemRequester, len(proxies)) ei := &injection.EnvironmentInjector{} @@ -80,6 +93,20 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi := &injection.EnvironmentInjector{} vi.Init() s.ei = vi + s.engineMode = opts.EngineMode + + if s.engineInUserMode() { + // create client pool + var initialCount int + if s.engineMode == types.EngineModeRepeatedUser { + initialCount = opts.MaxConcurrentIterCount + } else if s.engineMode == types.EngineModeDistinctUser { + initialCount = opts.IterationCount + } + s.cPool, err = NewClientPool(initialCount, opts.IterationCount, func() *http.Client { return &http.Client{} }) + } + // s.cPool will be nil otherwise + return } @@ -109,8 +136,22 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) + var client *http.Client + if s.engineInUserMode() { + // get client from pool + client = s.cPool.Get() + defer s.cPool.Put(client) + } + for _, sr := range requesters { - res := sr.requester.Send(envs) + var res *types.ScenarioStepResult + switch sr.requester.Type() { + case "HTTP": + httpRequester := sr.requester.(requester.HttpRequesterI) + res = httpRequester.Send(client, envs) + default: + res = &types.ScenarioStepResult{Err: types.RequestError{Type: fmt.Sprintf("type not defined: %s", sr.requester.Type())}} + } if res.Err.Type == types.ErrorProxy || res.Err.Type == types.ErrorIntented { err = &res.Err @@ -128,6 +169,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( enrichEnvFromPrevStep(envs, res.ExtractedEnvs) } + return } @@ -137,6 +179,13 @@ func enrichEnvFromPrevStep(m1 map[string]interface{}, m2 map[string]interface{}) } } +func (s *ScenarioService) engineInUserMode() bool { + if s.engineMode == types.EngineModeDistinctUser || s.engineMode == types.EngineModeRepeatedUser { + return true + } + return false +} + func (s *ScenarioService) enrichEnvFromData(envs map[string]interface{}) { var row map[string]interface{} sb := strings.Builder{} @@ -166,6 +215,10 @@ func (s *ScenarioService) Done() { r.requester.Done() } } + + if s.cPool != nil { + s.cPool.Done() + } } func (s *ScenarioService) getOrCreateRequesters(proxy *url.URL) (requesters []scenarioItemRequester, err error) { @@ -199,7 +252,14 @@ func (s *ScenarioService) createRequesters(proxy *url.URL) (err error) { }, ) - err = r.Init(s.ctx, si, proxy, s.debug, s.ei) + switch r.Type() { + case "HTTP": + httpRequester := r.(requester.HttpRequesterI) + err = httpRequester.Init(s.ctx, si, proxy, s.debug, s.ei) + default: + err = fmt.Errorf("type not defined: %s", r.Type()) + } + if err != nil { return } diff --git a/core/scenario/service_test.go b/core/scenario/service_test.go index 4bdae6dc..5ffe7231 100644 --- a/core/scenario/service_test.go +++ b/core/scenario/service_test.go @@ -23,6 +23,7 @@ package scenario import ( "context" "fmt" + "net/http" "net/url" "reflect" "testing" @@ -33,7 +34,7 @@ import ( "go.ddosify.com/ddosify/core/types" ) -type MockRequester struct { +type MockHttpRequester struct { InitCalled bool SendCalled bool DoneCalled bool @@ -46,7 +47,7 @@ type MockRequester struct { ReturnSend *types.ScenarioStepResult } -func (m *MockRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { +func (m *MockHttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { m.InitCalled = true if m.FailInit { return fmt.Errorf(m.FailInitMsg) @@ -54,15 +55,19 @@ func (m *MockRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd return } -func (m *MockRequester) Send(envs map[string]interface{}) (res *types.ScenarioStepResult) { +func (m *MockHttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { m.SendCalled = true return m.ReturnSend } -func (m *MockRequester) Done() { +func (m *MockHttpRequester) Done() { m.DoneCalled = true } +func (m *MockHttpRequester) Type() string { + return "HTTP" +} + type MockSleep struct { SleepCalled bool SleepCallCount int @@ -186,7 +191,11 @@ func TestInitService(t *testing.T) { // Act service := ScenarioService{} - err := service.Init(ctx, scenario, proxies, false) + err := service.Init(ctx, scenario, proxies, ScenarioOpts{ + Debug: false, + IterationCount: 1, + MaxConcurrentIterCount: 1, + }) // Assert if err != nil { @@ -226,19 +235,21 @@ func TestDo(t *testing.T) { { scenarioItemID: 1, sleeper: mockSleep, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}}, }, { scenarioItemID: 2, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}}, }, } + cPool, _ := NewClientPool(1, 1, func() *http.Client { return &http.Client{} }) service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: requesters, }, scenario: scenario, ctx: ctx, + cPool: cPool, } expectedResponse := types.ScenarioResult{ @@ -288,19 +299,19 @@ func TestDoErrorOnSend(t *testing.T) { requestersProxyError := []scenarioItemRequester{ { scenarioItemID: 1, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorProxy}}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorProxy}}}, }, } requestersIntentedError := []scenarioItemRequester{ { scenarioItemID: 1, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorIntented}}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorIntented}}}, }, } requestersConnError := []scenarioItemRequester{ { scenarioItemID: 1, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorConn}}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorConn}}}, }, } @@ -317,12 +328,14 @@ func TestDoErrorOnSend(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { + cPool, _ := NewClientPool(1, 1, func() *http.Client { return &http.Client{} }) service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: test.requesters, }, scenario: scenario, ctx: ctx, + cPool: cPool, } // Act @@ -405,10 +418,12 @@ func TestDone(t *testing.T) { p2, _ := url.Parse("http://proxy_server.com:8080") ctx := context.TODO() - requester1 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} - requester2 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} - requester3 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} - requester4 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} + cPool, _ := NewClientPool(1, 1, func() *http.Client { return &http.Client{} }) + + requester1 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} + requester2 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} + requester3 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} + requester4 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: { @@ -434,8 +449,8 @@ func TestDone(t *testing.T) { }, scenario: scenario, ctx: ctx, + cPool: cPool, } - // Act service.Done() @@ -473,7 +488,11 @@ func TestGetOrCreateRequesters(t *testing.T) { ctx := context.TODO() service := ScenarioService{} - service.Init(ctx, scenario, proxies, false) + service.Init(ctx, scenario, proxies, ScenarioOpts{ + Debug: false, + IterationCount: 1, + MaxConcurrentIterCount: 1, + }) expectedRequesters := []scenarioItemRequester{{scenarioItemID: 1, requester: &requester.HttpRequester{}}} expectedClients := map[*url.URL][]scenarioItemRequester{ @@ -518,7 +537,11 @@ func TestGetOrCreateRequestersNewProxy(t *testing.T) { ctx := context.TODO() service := ScenarioService{} - service.Init(ctx, scenario, proxies, false) + service.Init(ctx, scenario, proxies, ScenarioOpts{ + Debug: false, + IterationCount: 1, + MaxConcurrentIterCount: 1, + }) expectedRequesters := []scenarioItemRequester{{scenarioItemID: 1, requester: &requester.HttpRequester{}}} diff --git a/core/types/hammer.go b/core/types/hammer.go index 239ef05f..45bae2c0 100644 --- a/core/types/hammer.go +++ b/core/types/hammer.go @@ -35,6 +35,11 @@ const ( LoadTypeIncremental = "incremental" LoadTypeWaved = "waved" + // EngineModes + EngineModeDistinctUser = "distinct-user" + EngineModeRepeatedUser = "repeated-user" + EngineModeDdosify = "ddosify" + // Default Values DefaultIterCount = 100 DefaultLoadType = LoadTypeLinear @@ -46,6 +51,7 @@ const ( ) var loadTypes = [...]string{LoadTypeLinear, LoadTypeIncremental, LoadTypeWaved} +var engineModes = [...]string{EngineModeDdosify, EngineModeDistinctUser, EngineModeRepeatedUser} // TimeRunCount is the data structure to store manual load type data. type TimeRunCount []struct { @@ -101,6 +107,8 @@ type Hammer struct { // Sampling rate SamplingRate int + // Connection reuse + EngineMode string // Test Data Config TestDataConf map[string]CsvConf } @@ -116,6 +124,9 @@ func (h *Hammer) Validate() error { if h.LoadType != "" && !util.StringInSlice(h.LoadType, loadTypes[:]) { return fmt.Errorf("unsupported LoadType: %s", h.LoadType) } + if h.EngineMode != "" && !util.StringInSlice(h.EngineMode, engineModes[:]) { + return fmt.Errorf("unsupported EngineMode: %s", h.EngineMode) + } if len(h.TimeRunCountMap) > 0 { for _, t := range h.TimeRunCountMap { diff --git a/main_benchmark_test.go b/main_benchmark_test.go index ecf0bfb0..e4b26011 100644 --- a/main_benchmark_test.go +++ b/main_benchmark_test.go @@ -28,6 +28,7 @@ import ( "fmt" "log" "os" + "runtime" "runtime/pprof" "runtime/trace" "strconv" @@ -124,18 +125,22 @@ func BenchmarkEngines(t *testing.B) { i, _ := strconv.Atoi(index) conf := table[i] outSuffix := ".out" + var err error + // child proc + var cpuProfFile, memProfFile, traceFile *os.File if *cpuprofile != "" { - f, err := os.Create(fmt.Sprintf("%s_cpuprof_%s.out", strings.TrimSuffix(*cpuprofile, outSuffix), conf.name)) + cpuProfFile, err = os.Create(fmt.Sprintf("%s_cpuprof_%s.out", strings.TrimSuffix(*cpuprofile, outSuffix), conf.name)) if err != nil { log.Fatal(err) } - pprof.StartCPUProfile(f) + pprof.StartCPUProfile(cpuProfFile) + defer cpuProfFile.Close() defer pprof.StopCPUProfile() } if *memprofile != "" { // get memory profile at execution finish - memProfFile, err := os.Create(fmt.Sprintf("%s_memprof_%s.out", strings.TrimSuffix(*memprofile, outSuffix), + memProfFile, err = os.Create(fmt.Sprintf("%s_memprof_%s.out", strings.TrimSuffix(*memprofile, outSuffix), conf.name)) if err != nil { log.Fatal("could not create memory profile: ", err) @@ -150,17 +155,17 @@ func BenchmarkEngines(t *testing.B) { } if *keepTrace != "" { - f, err := os.Create(fmt.Sprintf("%s_trace_%s.out", strings.TrimSuffix(*keepTrace, outSuffix), conf.name)) + traceFile, err = os.Create(fmt.Sprintf("%s_trace_%s.out", strings.TrimSuffix(*keepTrace, outSuffix), conf.name)) if err != nil { log.Fatalf("failed to create trace output file: %v", err) } defer func() { - if err := f.Close(); err != nil { + if err := traceFile.Close(); err != nil { log.Fatalf("failed to close trace file: %v", err) } }() - if err := trace.Start(f); err != nil { + if err := trace.Start(traceFile); err != nil { log.Fatalf("failed to start trace: %v", err) } defer trace.Stop() @@ -214,10 +219,9 @@ func BenchmarkEngines(t *testing.B) { }) - if success { - os.Exit(0) + if !success { + runtime.Goexit() } - os.Exit(1) } }