diff --git a/slope-evaluator/README.md b/slope-evaluator/README.md index 12e8a61783532efce9e8722f500cf5ff880bace7..cd9e6820ed46452ce44d57d0c7e5cd5ae05e5a3b 100644 --- a/slope-evaluator/README.md +++ b/slope-evaluator/README.md @@ -5,7 +5,7 @@ For development: ```sh -uvicorn main:app --reload +uvicorn main:app --reload # run this command inside the app/ folder ``` ## Build the docker image: @@ -32,7 +32,7 @@ The running webserver provides a REST API with the following route: * /evaluate-slope * Method: POST * Body: - * total_lag + * total_lags * threshold * warmup @@ -40,14 +40,16 @@ The body of the request must be a JSON string that satisfies the following condi * **total_lag**: This property is based on the [Range Vector type](https://www.prometheus.io/docs/prometheus/latest/querying/api/#range-vectors) from Prometheus and must have the following JSON structure: ``` - { - "metric": { - "group": "<label_value>" - }, - "values": [ - [ - <unix_timestamp>, - "<sample_value>" + { + [ + "metric": { + "group": "<label_value>" + }, + "values": [ + [ + <unix_timestamp>, + "<sample_value>" + ] ] ] } diff --git a/slope-evaluator/app/main.py b/slope-evaluator/app/main.py index 5995a1175f781c4eb51ab3d3083a665fbc02d6fd..6f6788f0ca84b7710be5b509ca4f0641047e963d 100644 --- a/slope-evaluator/app/main.py +++ b/slope-evaluator/app/main.py @@ -5,6 +5,7 @@ import os import pandas as pd import json import sys +from statistics import median app = FastAPI() @@ -20,7 +21,7 @@ elif os.getenv('LOG_LEVEL') == 'WARNING': elif os.getenv('LOG_LEVEL') == 'DEBUG': logger.setLevel(logging.DEBUG) -def execute(results, threshold, warmup): +def calculate_slope_trend(results, warmup): d = [] for result in results: group = result['metric']['group'] @@ -39,13 +40,16 @@ def execute(results, threshold, warmup): logger.error('Mark this subexperiment as not successful and continue benchmark.') return False - result = trend_slope < threshold - logger.info("Computed lag trend slope is '%s'. Result is: %s", trend_slope, result) - return result + logger.info("Computed lag trend slope is '%s'", trend_slope) + return trend_slope + +def check_service_level_objective(results, threshold): + return median(results) < threshold @app.post("/evaluate-slope",response_model=bool) async def evaluate_slope(request: Request): data = json.loads(await request.body()) - return execute(data['total_lag'], data['threshold'], data['warmup']) + results = [calculate_slope_trend(total_lag, data['warmup']) for total_lag in data['total_lags']] + return check_service_level_objective(results=results, threshold=data["threshold"]) -logger.info("Slope evaluator is online") \ No newline at end of file +logger.info("SLO evaluator is online") \ No newline at end of file diff --git a/slope-evaluator/app/test.py b/slope-evaluator/app/test.py new file mode 100644 index 0000000000000000000000000000000000000000..9b165ea479bb9a552edaba7692df4fd4ef3f4ab4 --- /dev/null +++ b/slope-evaluator/app/test.py @@ -0,0 +1,30 @@ +import unittest +from main import app, check_service_level_objective +import json +from fastapi.testclient import TestClient + +class TestSloEvaluation(unittest.TestCase): + client = TestClient(app) + + def test_1_rep(self): + with open('../resources/test-1-rep-success.json') as json_file: + data = json.load(json_file) + response = self.client.post("/evaluate-slope", json=data) + self.assertEquals(response.json(), True) + + def test_3_rep(self): + with open('../resources/test-3-rep-success.json') as json_file: + data = json.load(json_file) + response = self.client.post("/evaluate-slope", json=data) + self.assertEquals(response.json(), True) + + def test_check_service_level_objective(self): + list = [1,2,3,4] + self.assertEquals(check_service_level_objective(list, 2), False) + self.assertEquals(check_service_level_objective(list, 3), True) + list = [1,2,3,4,5] + self.assertEquals(check_service_level_objective(list, 2), False) + self.assertEquals(check_service_level_objective(list, 4), True) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/slope-evaluator/requirements.txt b/slope-evaluator/requirements.txt index ca77b6c891136b1388aaf56c5ae269d6ee4b5729..6934f4b780a4b7c558c5ce8f1718171e8bec4586 100644 --- a/slope-evaluator/requirements.txt +++ b/slope-evaluator/requirements.txt @@ -1,3 +1,4 @@ fastapi==0.55.1 scikit-learn==0.20.3 pandas==1.0.3 +uvicorn diff --git a/slope-evaluator/resources/test-1-rep-success.json b/slope-evaluator/resources/test-1-rep-success.json new file mode 100644 index 0000000000000000000000000000000000000000..9e315c707be7b2a874c58fcb1093aa86f7676560 --- /dev/null +++ b/slope-evaluator/resources/test-1-rep-success.json @@ -0,0 +1,139 @@ +{ + "total_lags": [ + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621008960827E9, + "234" + ], + [ + 1.621008965827E9, + "234" + ], + [ + 1.621008970827E9, + "234" + ], + [ + 1.621008975827E9, + "719" + ], + [ + 1.621008980827E9, + "719" + ], + [ + 1.621008985827E9, + "719" + ], + [ + 1.621008990827E9, + "1026" + ], + [ + 1.621008995827E9, + "1026" + ], + [ + 1.621009000827E9, + "1026" + ], + [ + 1.621009005827E9, + "534" + ], + [ + 1.621009010827E9, + "534" + ], + [ + 1.621009015827E9, + "534" + ], + [ + 1.621009020827E9, + "943" + ], + [ + 1.621009025827E9, + "943" + ], + [ + 1.621009030827E9, + "943" + ], + [ + 1.621009035827E9, + "66" + ], + [ + 1.621009040827E9, + "66" + ], + [ + 1.621009045827E9, + "66" + ], + [ + 1.621009050827E9, + "841" + ], + [ + 1.621009055827E9, + "841" + ], + [ + 1.621009060827E9, + "841" + ], + [ + 1.621009065827E9, + "405" + ], + [ + 1.621009070827E9, + "405" + ], + [ + 1.621009075827E9, + "405" + ], + [ + 1.621009080827E9, + "201" + ], + [ + 1.621009085827E9, + "201" + ], + [ + 1.621009090827E9, + "201" + ], + [ + 1.621009095827E9, + "227" + ], + [ + 1.621009100827E9, + "227" + ], + [ + 1.621009105827E9, + "227" + ], + [ + 1.621009110827E9, + "943" + ] + ] + } + ] + ], + "threshold": 2000, + "warmup": 0 +} \ No newline at end of file diff --git a/slope-evaluator/resources/test-3-rep-success.json b/slope-evaluator/resources/test-3-rep-success.json new file mode 100644 index 0000000000000000000000000000000000000000..485966cba40f01e4a646e626914510ba49b707bc --- /dev/null +++ b/slope-evaluator/resources/test-3-rep-success.json @@ -0,0 +1,289 @@ +{ + "total_lags": [ + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621012384232E9, + "6073" + ], + [ + 1.621012389232E9, + "6073" + ], + [ + 1.621012394232E9, + "6073" + ], + [ + 1.621012399232E9, + "227" + ], + [ + 1.621012404232E9, + "227" + ], + [ + 1.621012409232E9, + "227" + ], + [ + 1.621012414232E9, + "987" + ], + [ + 1.621012419232E9, + "987" + ], + [ + 1.621012424232E9, + "987" + ], + [ + 1.621012429232E9, + "100" + ], + [ + 1.621012434232E9, + "100" + ], + [ + 1.621012439232E9, + "100" + ], + [ + 1.621012444232E9, + "959" + ], + [ + 1.621012449232E9, + "959" + ], + [ + 1.621012454232E9, + "959" + ], + [ + 1.621012459232E9, + "625" + ], + [ + 1.621012464232E9, + "625" + ], + [ + 1.621012469232E9, + "625" + ], + [ + 1.621012474232E9, + "683" + ], + [ + 1.621012479232E9, + "683" + ], + [ + 1.621012484232E9, + "683" + ], + [ + 1.621012489232E9, + "156" + ] + ] + } + ], + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621012545211E9, + "446" + ], + [ + 1.621012550211E9, + "446" + ], + [ + 1.621012555211E9, + "446" + ], + [ + 1.621012560211E9, + "801" + ], + [ + 1.621012565211E9, + "801" + ], + [ + 1.621012570211E9, + "801" + ], + [ + 1.621012575211E9, + "773" + ], + [ + 1.621012580211E9, + "773" + ], + [ + 1.621012585211E9, + "773" + ], + [ + 1.621012590211E9, + "509" + ], + [ + 1.621012595211E9, + "509" + ], + [ + 1.621012600211E9, + "509" + ], + [ + 1.621012605211E9, + "736" + ], + [ + 1.621012610211E9, + "736" + ], + [ + 1.621012615211E9, + "736" + ], + [ + 1.621012620211E9, + "903" + ], + [ + 1.621012625211E9, + "903" + ], + [ + 1.621012630211E9, + "903" + ], + [ + 1.621012635211E9, + "512" + ], + [ + 1.621012640211E9, + "512" + ], + [ + 1.621012645211E9, + "512" + ] + ] + } + ], + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621012700748E9, + "6484" + ], + [ + 1.621012705748E9, + "6484" + ], + [ + 1.621012710748E9, + "6484" + ], + [ + 1.621012715748E9, + "505" + ], + [ + 1.621012720748E9, + "505" + ], + [ + 1.621012725748E9, + "505" + ], + [ + 1.621012730748E9, + "103" + ], + [ + 1.621012735748E9, + "103" + ], + [ + 1.621012740748E9, + "103" + ], + [ + 1.621012745748E9, + "201" + ], + [ + 1.621012750748E9, + "201" + ], + [ + 1.621012755748E9, + "201" + ], + [ + 1.621012760748E9, + "965" + ], + [ + 1.621012765748E9, + "965" + ], + [ + 1.621012770748E9, + "965" + ], + [ + 1.621012775748E9, + "876" + ], + [ + 1.621012780748E9, + "876" + ], + [ + 1.621012785748E9, + "876" + ], + [ + 1.621012790748E9, + "380" + ], + [ + 1.621012795748E9, + "380" + ], + [ + 1.621012800748E9, + "380" + ] + ] + } + ] + ], + "threshold": 2000, + "warmup": 0 +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt index c5d691eb8f1437726bcce22500ad995bc1b6e4da..59a793bd7c6d2897fc715c58deda54c178d160f4 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt @@ -34,25 +34,27 @@ class AnalysisExecutor( * @param executionDuration of the experiment. * @return true if the experiment succeeded. */ - fun analyze(load: LoadDimension, res: Resource, executionDuration: Duration): Boolean { + fun analyze(load: LoadDimension, res: Resource, executionIntervals: List<Pair<Instant, Instant>>): Boolean { var result = false + val exporter = CsvExporter() + var repetitionCounter = 1 try { - val prometheusData = fetcher.fetchMetric( - start = Instant.now().minus(executionDuration), - end = Instant.now(), - query = "sum by(group)(kafka_consumergroup_group_lag >= 0)" - ) - - var resultsFolder: String = System.getenv("RESULTS_FOLDER") + var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" if (resultsFolder.isNotEmpty()){ resultsFolder += "/" } + val prometheusData = executionIntervals + .map { interval -> fetcher.fetchMetric( + start = interval.first, + end = interval.second, + query = "sum by(group)(kafka_consumergroup_group_lag >= 0)") } + + val fileName= "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}" + prometheusData.forEach{ data -> + exporter.toCsv(name = "${fileName}_${repetitionCounter++}", prom = data) } + - CsvExporter().toCsv( - name = "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}", - prom = prometheusData - ) val sloChecker = SloCheckerFactory().create( sloType = slo.sloType, externalSlopeURL = slo.externalSloUrl, @@ -60,10 +62,7 @@ class AnalysisExecutor( warmup = slo.warmup ) - result = sloChecker.evaluate( - start = Instant.now().minus(executionDuration), - end = Instant.now(), fetchedData = prometheusData - ) + result = sloChecker.evaluate(prometheusData) } catch (e: Exception) { logger.error { "Evaluation failed for resource '${res.get()}' and load '${load.get()}'. Error: $e" } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt index 8eac05a815e0bc45e1abc38bcdf0352e61bd7730..f7ebee8faf740583dbe6a37381a599e9bde19280 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt @@ -35,10 +35,10 @@ class ExternalSloChecker( * @return true if the experiment was successful(the threshold was not exceeded. * @throws ConnectException if the external service could not be reached. */ - override fun evaluate(start: Instant, end: Instant, fetchedData: PrometheusResponse): Boolean { + override fun evaluate(fetchedData: List<PrometheusResponse>): Boolean { var counter = 0 val data = Gson().toJson(mapOf( - "total_lag" to fetchedData.data?.result, + "total_lags" to fetchedData.map { it.data?.result}, "threshold" to threshold, "warmup" to warmup)) diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt index 758dcdefcaee79654d8ff65f0f798832aafc1294..9ee5fe7ef34ce5b6214882ce2c1d19677f1d7130 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt @@ -1,14 +1,12 @@ package theodolite.evaluation import theodolite.util.PrometheusResponse -import java.time.Instant /** * A SloChecker can be used to evaluate data from Prometheus. * @constructor Creates an empty SloChecker */ interface SloChecker { - /** * Evaluates [fetchedData] and returns if the experiment was successful. * Returns if the evaluated experiment was successful. @@ -18,5 +16,5 @@ interface SloChecker { * @param fetchedData from Prometheus that will be evaluated. * @return true if experiment was successful. Otherwise false. */ - fun evaluate(start: Instant, end: Instant, fetchedData: PrometheusResponse): Boolean + fun evaluate(fetchedData: List<PrometheusResponse>): Boolean } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt index 494e52878ce6bfe5f7ad57ebf4c9030db5a66a55..e7b511d8c83b5abccece1204aad2a4a9ecfdfd26 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt @@ -26,6 +26,7 @@ abstract class BenchmarkExecutor( val executionDuration: Duration, val configurationOverrides: List<ConfigurationOverride?>, val slo: BenchmarkExecution.Slo, + val repetitions: Int, val executionId: Int, val loadGenerationDelay: Long, val afterTeardownDelay: Long diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt index 6237af7fc78b03d3dca5941f1d8f9d9b9ea58246..3afc85f0a8cb67011763498a662b447ce2c07f0f 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt @@ -5,11 +5,9 @@ import mu.KotlinLogging import theodolite.benchmark.Benchmark import theodolite.benchmark.BenchmarkExecution import theodolite.evaluation.AnalysisExecutor -import theodolite.util.ConfigurationOverride -import theodolite.util.LoadDimension -import theodolite.util.Resource -import theodolite.util.Results +import theodolite.util.* import java.time.Duration +import java.time.Instant private val logger = KotlinLogging.logger {} @@ -20,42 +18,56 @@ class BenchmarkExecutorImpl( executionDuration: Duration, configurationOverrides: List<ConfigurationOverride?>, slo: BenchmarkExecution.Slo, + repetitions: Int, executionId: Int, loadGenerationDelay: Long, afterTeardownDelay: Long -) : BenchmarkExecutor(benchmark, results, executionDuration, configurationOverrides, slo, executionId, loadGenerationDelay, afterTeardownDelay) { +) : BenchmarkExecutor(benchmark, results, executionDuration, configurationOverrides, slo, repetitions, executionId, loadGenerationDelay, afterTeardownDelay) { override fun runExperiment(load: LoadDimension, res: Resource): Boolean { var result = false - val benchmarkDeployment = benchmark.buildDeployment(load, res, configurationOverrides, loadGenerationDelay, this.afterTeardownDelay) + val executionIntervals: MutableList<Pair<Instant, Instant>> = ArrayList() - try { - benchmarkDeployment.setup() - this.waitAndLog() - } catch (e: Exception) { - logger.error { "Error while setting up experiment with id ${this.executionId}." } - logger.error { "Error is: $e" } - this.run.set(false) + for (i in 1.rangeTo(repetitions)) { + logger.info { "Run repetition $i/$repetitions" } + if (this.run.get()) { + executionIntervals.add(runSingleExperiment(load,res)) + } else { + break + } } /** * Analyse the experiment, if [run] is true, otherwise the experiment was canceled by the user. */ if (this.run.get()) { - result = AnalysisExecutor(slo = slo, executionId = executionId).analyze( - load = load, - res = res, - executionDuration = executionDuration - ) + result =AnalysisExecutor(slo = slo, executionId = executionId) + .analyze( + load = load, + res = res, + executionIntervals = executionIntervals) this.results.setResult(Pair(load, res), result) } + return result + } + private fun runSingleExperiment(load: LoadDimension, res: Resource): Pair<Instant, Instant> { + val benchmarkDeployment = benchmark.buildDeployment(load, res, this.configurationOverrides, this.loadGenerationDelay, this.afterTeardownDelay) + val from = Instant.now() + try { + benchmarkDeployment.setup() + this.waitAndLog() + } catch (e: Exception) { + logger.error { "Error while setup experiment." } + logger.error { "Error is: $e" } + this.run.set(false) + } + val to = Instant.now() try { benchmarkDeployment.teardown() } catch (e: Exception) { logger.warn { "Error while tearing down the benchmark deployment." } logger.debug { "Teardown failed, caused by: $e" } } - - return result + return Pair(from,to) } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index 34fbc2f8a3cc5934f8d49c6ebac053fbd91e5551..ded4943c14ff8250f3dcad5279ff670af5fd7fd3 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt @@ -71,6 +71,7 @@ class TheodoliteExecutor( executionDuration = executionDuration, configurationOverrides = config.configOverrides, slo = config.slos[0], + repetitions = config.execution.repetitions, executionId = config.executionId, loadGenerationDelay = config.execution.loadGenerationDelay, afterTeardownDelay = config.execution.afterTeardownDelay diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt index 8b2909f7658f4dffcfd961cad8cd00eb013a160c..b9977029703c8012ada7fb3d7766bfa321a836c3 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt @@ -31,8 +31,8 @@ class TheodoliteYamlExecutor { fun start() { logger.info { "Theodolite started" } - val executionPath = System.getenv("THEODOLITE_EXECUTION") ?: "./config/BenchmarkExecution.yaml" - val benchmarkPath = System.getenv("THEODOLITE_BENCHMARK") ?: "./config/BenchmarkType.yaml" + val executionPath = System.getenv("THEODOLITE_EXECUTION") ?: "./config/example-execution-yaml-resource.yaml" + val benchmarkPath = System.getenv("THEODOLITE_BENCHMARK") ?: "./config/example-benchmark-yaml-resource.yaml" logger.info { "Using $executionPath for BenchmarkExecution" } logger.info { "Using $benchmarkPath for BenchmarkType" } diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt index 91c312261bda83923d068b7dfce67e36afd0ebfb..cbd2d5926d61b0bfd4de6fab0c14422ddf88f190 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt @@ -23,6 +23,7 @@ class TestBenchmarkExecutorImpl( executionDuration = Duration.ofSeconds(1), configurationOverrides = emptyList(), slo = slo, + repetitions = 1, executionId = executionId, loadGenerationDelay = loadGenerationDelay, afterTeardownDelay = afterTeardownDelay