diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0d351cc8ec3a9a9400867ec6462e5390c575f3d..9b10ffeabbc08a1f25a88d2b351f3e8dd6309443 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -213,7 +213,7 @@ deploy-uc4-load-generator: # Theodolite Framework .theodolite: - image: openjdk:11-jdk + image: ghcr.io/graalvm/graalvm-ce:java11-21.0.0.2 tags: - exec-docker variables: @@ -226,23 +226,33 @@ deploy-uc4-load-generator: - cd theodolite-quarkus - export GRADLE_USER_HOME=`pwd`/.gradle -build-theodolite: +build-theodolite-jvm: stage: build extends: .theodolite - # script: ./gradlew --build-cache assemble -Dquarkus.package.type=native script: ./gradlew --build-cache assemble artifacts: paths: - "theodolite-quarkus/build/lib/*" - "theodolite-quarkus/build/*-runner.jar" - # - "theodolite-quarkus/build/*-runner" # For native image + expire_in: 1 day + +build-theodolite-native: + stage: build + extends: .theodolite + script: + - gu install native-image # TODO move to image + - ./gradlew --build-cache assemble -Dquarkus.package.type=native + artifacts: + paths: + - "theodolite-quarkus/build/*-runner" expire_in: 1 day test-theodolite: stage: test extends: .theodolite needs: - - build-theodolite + - build-theodolite-jvm + - build-theodolite-native script: ./gradlew test --stacktrace # Disabled for now @@ -250,7 +260,7 @@ test-theodolite: stage: check extends: .theodolite needs: - - build-theodolite + - build-theodolite-jvm - test-theodolite script: ./gradlew ktlintCheck --continue @@ -259,7 +269,7 @@ test-theodolite: stage: check extends: .theodolite needs: - - build-theodolite + - build-theodolite-jvm - test-theodolite script: ./gradlew detekt --continue @@ -269,12 +279,12 @@ deploy-theodolite: - .theodolite - .dind needs: - - build-theodolite + - build-theodolite-native - test-theodolite script: - DOCKER_TAG_NAME=$(echo $CI_COMMIT_REF_SLUG- | sed 's/^master-$//') - #- docker build -f src/main/docker/Dockerfile.native -t theodolite . - - docker build -f src/main/docker/Dockerfile.jvm -t theodolite . + - docker build -f src/main/docker/Dockerfile.native -t theodolite . + #- docker build -f src/main/docker/Dockerfile.jvm -t theodolite . - "[ ! $CI_COMMIT_TAG ] && docker tag theodolite $CR_HOST/$CR_ORG/theodolite:${DOCKER_TAG_NAME}latest" - "[ ! $CI_COMMIT_TAG ] && docker tag theodolite $CR_HOST/$CR_ORG/theodolite:$DOCKER_TAG_NAME$CI_COMMIT_SHORT_SHA" - "[ $CI_COMMIT_TAG ] && docker tag theodolite $CR_HOST/$CR_ORG/theodolite:$CI_COMMIT_TAG" diff --git a/slope-evaluator/Dockerfile b/slope-evaluator/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..032b8153a6989ca04631ba553289dacb3620a38d --- /dev/null +++ b/slope-evaluator/Dockerfile @@ -0,0 +1,6 @@ +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY ./app /app \ No newline at end of file diff --git a/slope-evaluator/README.md b/slope-evaluator/README.md new file mode 100644 index 0000000000000000000000000000000000000000..25c02b42e6a6eb4611972febf935403b8b8703c8 --- /dev/null +++ b/slope-evaluator/README.md @@ -0,0 +1,26 @@ +# Lag Trend SLO Evaluator + +## Execution + +For development: + +```sh +uvicorn main:app --reload +``` + +Build the docker image: + +```sh +docker build . -t theodolite-evaluator +``` + +Run the Docker image: + +```sh + docker run -p 80:80 theodolite-evaluator +``` + +## Configuration + +You can set the `HOST` and the `PORT` (and a lot of more parameters) via environment variables. Default is `0.0.0.0:80`. +For more information see [here](https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#advanced-usage). diff --git a/slope-evaluator/app/main.py b/slope-evaluator/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..83709c0f71563d9bd1c29c5f064645144163ea72 --- /dev/null +++ b/slope-evaluator/app/main.py @@ -0,0 +1,51 @@ +from fastapi import FastAPI,Request +import trend_slope_computer as trend_slope_computer +import logging +import os +import pandas as pd +import json +import sys + +app = FastAPI() + +logging.basicConfig(stream=sys.stdout, + format="%(asctime)s %(levelname)s %(name)s: %(message)s") +logger = logging.getLogger("API") + + +if os.getenv('LOG_LEVEL') == 'INFO': + logger.setLevel(logging.INFO) +elif os.getenv('LOG_LEVEL') == 'WARNING': + logger.setLevel(logging.WARNING) +elif os.getenv('LOG_LEVEL') == 'DEBUG': + logger.setLevel((logging.DEBUG)) + +def execute(results, threshold, warmup): + d = [] + for result in results: + group = result['metric']['group'] + for value in result['values']: + d.append({'group': group, 'timestamp': int( + value[0]), 'value': int(value[1]) if value[1] != 'NaN' else 0}) + + df = pd.DataFrame(d) + + logger.info(df) + try: + trend_slope = trend_slope_computer.compute(df, warmup) + except Exception as e: + err_msg = 'Computing trend slope failed' + logger.exception(err_msg) + logger.error('Mark this subexperiment as not successful and continue benchmark') + return False + + logger.info("Trend Slope: %s", trend_slope) + + return trend_slope < 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']) + +logger.info("Slope evaluator is online") \ No newline at end of file diff --git a/slope-evaluator/app/trend_slope_computer.py b/slope-evaluator/app/trend_slope_computer.py new file mode 100644 index 0000000000000000000000000000000000000000..c128d9f48c1e7ba20e43dfbfd6a0391eeec2b60b --- /dev/null +++ b/slope-evaluator/app/trend_slope_computer.py @@ -0,0 +1,18 @@ +from sklearn.linear_model import LinearRegression +import pandas as pd +import os + +def compute(x, warmup_sec): + input = x + input['sec_start'] = input.loc[0:, 'timestamp'] - input.iloc[0]['timestamp'] + regress = input.loc[input['sec_start'] >= warmup_sec] # Warm-Up + + X = regress.iloc[:, 2].values.reshape(-1, 1) # values converts it into a numpy array + Y = regress.iloc[:, 3].values.reshape(-1, 1) # -1 means that calculate the dimension of rows, but have 1 column + linear_regressor = LinearRegression() # create object for the class + linear_regressor.fit(X, Y) # perform linear regression + Y_pred = linear_regressor.predict(X) # make predictions + + trend_slope = linear_regressor.coef_[0][0] + + return trend_slope diff --git a/slope-evaluator/requirements.txt b/slope-evaluator/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca77b6c891136b1388aaf56c5ae269d6ee4b5729 --- /dev/null +++ b/slope-evaluator/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.55.1 +scikit-learn==0.20.3 +pandas==1.0.3 diff --git a/theodolite-quarkus/build.gradle b/theodolite-quarkus/build.gradle index ba80ced4b1e4e58f34d5b316f4a46f4e032654a9..1ceb0937776299e6fc9a3c0ec470c9e320e6790f 100644 --- a/theodolite-quarkus/build.gradle +++ b/theodolite-quarkus/build.gradle @@ -20,12 +20,13 @@ dependencies { implementation 'io.quarkus:quarkus-resteasy' testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' + implementation 'com.google.code.gson:gson:2.8.5' implementation 'org.slf4j:slf4j-simple:1.7.29' implementation 'io.github.microutils:kotlin-logging:1.12.0' implementation 'io.fabric8:kubernetes-client:5.0.0-alpha-2' - compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.7.0' - compile group: 'org.apache.zookeeper', name: 'zookeeper', version: '3.6.2' + implementation 'org.apache.kafka:kafka-clients:2.7.0' + implementation 'khttp:khttp:1.0.0' } group 'theodolite' diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt index 32b06d10f609b8898ac94d514baf4f293b1a2c97..25535e1a64db9641cd47747cf8676b3994964690 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt @@ -22,6 +22,10 @@ class BenchmarkExecution { class Slo { lateinit var sloType: String var threshold by Delegates.notNull<Int>() + lateinit var prometheusUrl: String + lateinit var externalSloUrl: String + var offset by Delegates.notNull<Int>() + var warmup by Delegates.notNull<Int>() } class LoadDefinition { diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt index 1e2d9a2e675bee3817c493e5afc89333ca508ed8..0110e1d7cdbbe150fc6d76bc303770b989f5d739 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.client.DefaultKubernetesClient import mu.KotlinLogging import theodolite.k8s.K8sResourceLoader -import theodolite.patcher.PatcherManager +import theodolite.patcher.PatcherFactory import theodolite.util.* private val logger = KotlinLogging.logger {} @@ -43,20 +43,15 @@ class KubernetesBenchmark : Benchmark { configurationOverrides: List<ConfigurationOverride> ): BenchmarkDeployment { val resources = loadKubernetesResources(this.appResource + this.loadGenResource) - val patcherManager = PatcherManager() + val patcherFactory = PatcherFactory() - // patch res and load - patcherManager.createAndApplyPatcher(res.getType(), this.resourceTypes, resources, res.get()) - patcherManager.createAndApplyPatcher(load.getType(), this.loadTypes, resources, load.get().toString()) + // patch the load dimension the resources + load.getType().forEach { patcherDefinition -> patcherFactory.createPatcher(patcherDefinition, resources).patch(load.get().toString()) } + res.getType().forEach{ patcherDefinition -> patcherFactory.createPatcher(patcherDefinition, resources).patch(res.get().toString()) } + + // Patch the given overrides + configurationOverrides.forEach { override -> patcherFactory.createPatcher(override.patcher, resources).patch(override.value) } - // patch overrides - configurationOverrides.forEach { override -> - patcherManager.applyPatcher( - listOf(override.patcher), - resources, - override.value - ) - } return KubernetesBenchmarkDeployment( namespace = namespace, diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt new file mode 100644 index 0000000000000000000000000000000000000000..2de8e2dc9c03ec5449c9f04585622d6730644aa2 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt @@ -0,0 +1,40 @@ +package theodolite.evaluation + +import com.google.gson.Gson +import khttp.post +import java.net.ConnectException +import java.time.Duration +import java.time.Instant + +class ExternalSloChecker( + private val prometheusURL: String, + private val query: String, + private val externalSlopeURL: String, + private val threshold: Int, + private val offset: Duration, + private val warmup: Int +) : + SloChecker { + + private val RETRIES = 2 + private val TIMEOUT = 60.0 + + override fun evaluate(start: Instant, end: Instant): Boolean { + var counter = 0 + val metricFetcher = MetricFetcher(prometheusURL = prometheusURL, offset = offset) + val fetchedData = metricFetcher.fetchMetric(start, end, query) + val data = + Gson().toJson(mapOf("total_lag" to fetchedData.data?.result, "threshold" to threshold, "warmup" to warmup)) + + while (counter < RETRIES) { + val result = post(externalSlopeURL, data = data, timeout = TIMEOUT) + if (result.statusCode != 200) { + counter++ + } else { + return result.text.toBoolean() + } + } + + throw ConnectException("Could not reach slope evaluation") + } +} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/MetricFetcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/MetricFetcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..7dbaf568c3452e7ae565002ae00e5314502f8930 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/MetricFetcher.kt @@ -0,0 +1,55 @@ +package theodolite.evaluation + +import com.google.gson.Gson +import khttp.get +import khttp.responses.Response +import mu.KotlinLogging +import theodolite.util.PrometheusResponse +import java.net.ConnectException +import java.time.Duration +import java.time.Instant + +private val logger = KotlinLogging.logger {} + +class MetricFetcher(private val prometheusURL: String, private val offset: Duration) { + private val RETRIES = 2 + private val TIMEOUT = 60.0 + + fun fetchMetric(start: Instant, end: Instant, query: String): PrometheusResponse { + + val offsetStart = start.minus(offset) + val offsetEnd = end.minus(offset) + + var counter = 0 + val parameter = mapOf( + "query" to query, + "start" to offsetStart.toString(), + "end" to offsetEnd.toString(), + "step" to "5s" + ) + + while (counter < RETRIES) { + val response = get("$prometheusURL/api/v1/query_range", params = parameter, timeout = TIMEOUT) + if (response.statusCode != 200) { + val message = response.jsonObject.toString() + logger.warn { "Could not connect to Prometheus: $message, retrying now" } + counter++ + } else { + val values = parseValues(response) + if (values.data?.result.isNullOrEmpty()) { + logger.error { "Empty query result: $values" } + throw NoSuchFieldException() + } + return parseValues(response) + } + } + throw ConnectException("No answer from Prometheus received") + } + + private fun parseValues(values: Response): PrometheusResponse { + return Gson().fromJson<PrometheusResponse>( + values.jsonObject.toString(), + PrometheusResponse::class.java + ) + } +} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SLOChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SLOChecker.kt deleted file mode 100644 index 396e1864b491e15d44881439c10847a39ea18286..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SLOChecker.kt +++ /dev/null @@ -1,3 +0,0 @@ -package theodolite.evaluation - -interface SLOChecker {} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt new file mode 100644 index 0000000000000000000000000000000000000000..53ed1b7fa02681f97b121f93d690c0654f961a94 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt @@ -0,0 +1,7 @@ +package theodolite.evaluation + +import java.time.Instant + +interface SloChecker { + fun evaluate(start: Instant, end: Instant): Boolean +} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..2170ef7b6abdb74499d05ac623c7892ac36b72d9 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt @@ -0,0 +1,29 @@ +package theodolite.evaluation + +import java.time.Duration + +class SloCheckerFactory { + + fun create( + slotype: String, + prometheusURL: String, + query: String, + externalSlopeURL: String, + threshold: Int, + offset: Duration, + warmup: Int + ): SloChecker { + + return when (slotype) { + "lag trend" -> ExternalSloChecker( + prometheusURL = prometheusURL, + query = query, + externalSlopeURL = externalSlopeURL, + threshold = threshold, + offset = offset, + warmup = warmup + ) + else -> throw IllegalArgumentException("Slotype $slotype not found.") + } + } +} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt index 1e939812298da33c47513b3322ff33c00ea001be..d3c2fdcbc0274066e62dd2dfe01fd2a8cf940f13 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt @@ -2,6 +2,7 @@ package theodolite.execution import mu.KotlinLogging import theodolite.benchmark.Benchmark +import theodolite.benchmark.BenchmarkExecution import theodolite.util.ConfigurationOverride import theodolite.util.LoadDimension import theodolite.util.Resource @@ -22,7 +23,8 @@ abstract class BenchmarkExecutor( val benchmark: Benchmark, val results: Results, val executionDuration: Duration, - configurationOverrides: List<ConfigurationOverride> + configurationOverrides: List<ConfigurationOverride>, + val slo: BenchmarkExecution.Slo ) { /** @@ -39,9 +41,13 @@ abstract class BenchmarkExecutor( * */ fun waitAndLog() { - for (i in 1.rangeTo(executionDuration.toMinutes())) { - Thread.sleep(Duration.ofMinutes(1).toMillis()) - logger.info { "Executed: $i minutes" } + logger.info { "Execution of a new benchmark started." } + for (i in 1.rangeTo(executionDuration.toSeconds())) { + + Thread.sleep(Duration.ofSeconds(1).toMillis()) + if ((i % 60) == 0L) { + logger.info { "Executed: ${i / 60} minutes" } + } } } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt index 45616a100a8ed067237413ac5afa9fd32f4865e1..19a0cb61c7bc24a00b1c769e77f4174d2f09d2d9 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt @@ -1,25 +1,51 @@ package theodolite.execution +import mu.KotlinLogging import theodolite.benchmark.Benchmark +import theodolite.benchmark.BenchmarkExecution +import theodolite.evaluation.SloCheckerFactory import theodolite.util.ConfigurationOverride import theodolite.util.LoadDimension import theodolite.util.Resource import theodolite.util.Results import java.time.Duration +import java.time.Instant + +private val logger = KotlinLogging.logger {} class BenchmarkExecutorImpl( benchmark: Benchmark, results: Results, executionDuration: Duration, - private val configurationOverrides: List<ConfigurationOverride> -) : BenchmarkExecutor(benchmark, results, executionDuration, configurationOverrides) { + private val configurationOverrides: List<ConfigurationOverride>, + slo: BenchmarkExecution.Slo +) : BenchmarkExecutor(benchmark, results, executionDuration, configurationOverrides, slo) { + //TODO ADD SHUTDOWN HOOK HERE override fun runExperiment(load: LoadDimension, res: Resource): Boolean { val benchmarkDeployment = benchmark.buildDeployment(load, res, this.configurationOverrides) benchmarkDeployment.setup() this.waitAndLog() benchmarkDeployment.teardown() - // todo evaluate - val result = false // if success else false + + var result = false + try { + result = SloCheckerFactory().create( + slotype = slo.sloType, + prometheusURL = slo.prometheusUrl, + query = "sum by(group)(kafka_consumergroup_group_lag >= 0)", + externalSlopeURL = slo.externalSloUrl, + threshold = slo.threshold, + offset = Duration.ofHours(slo.offset.toLong()), + warmup = slo.warmup + ) + .evaluate( + Instant.now().minus(executionDuration), + Instant.now() + ) + } catch (e: Exception) { + logger.error { "Evaluation failed for resource: ${res.get()} and load: ${load.get()} error: $e" } + } + this.results.setResult(Pair(load, res), result) return result } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index 87388cb61cf8fcb93a1028d20d2e3b68d322ab3d..89a3fb4fe5e0f81aa12aa566c9dbb2630c9b9bfe 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt @@ -2,6 +2,7 @@ package theodolite.execution import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark +import theodolite.patcher.PatcherDefinitionFactory import theodolite.strategies.StrategyFactory import theodolite.strategies.searchstrategy.CompositeStrategy import theodolite.util.Config @@ -20,12 +21,21 @@ class TheodoliteExecutor( val strategyFactory = StrategyFactory() val executionDuration = Duration.ofSeconds(config.execution.duration) - val executor = BenchmarkExecutorImpl(kubernetesBenchmark, results, executionDuration, config.configOverrides) + val resourcePatcherDefinition = PatcherDefinitionFactory().createPatcherDefinition(config.resources.resourceType, this.kubernetesBenchmark.resourceTypes) + val loadDimensionPatcherDefinition = PatcherDefinitionFactory().createPatcherDefinition(config.load.loadType, this.kubernetesBenchmark.loadTypes) + + val executor = + BenchmarkExecutorImpl( + kubernetesBenchmark, + results, + executionDuration, + config.configOverrides, + config.slos[0] + ) return Config( - loads = config.load.loadValues.map { load -> LoadDimension(load, config.load.loadType) }, - resources = config.resources.resourceValues.map - { resource -> Resource(resource, config.resources.resourceType) }, + loads = config.load.loadValues.map { load -> LoadDimension(load, loadDimensionPatcherDefinition) }, + resources = config.resources.resourceValues.map { resource -> Resource(resource, resourcePatcherDefinition) }, compositeStrategy = CompositeStrategy( benchmarkExecutor = executor, searchStrategy = strategyFactory.createSearchStrategy(executor, config.execution.strategy), diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt index ecc202542a0472808f70f9c5dd9696e2de370ea1..f6c109dac52ea4a4faeb30b8041329bc86812552 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt @@ -5,6 +5,8 @@ import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark import theodolite.util.YamlParser +import kotlin.system.exitProcess + private val logger = KotlinLogging.logger {} @QuarkusMain(name = "TheodoliteYamlExecutor") @@ -23,5 +25,6 @@ object TheodoliteYamlExecutor { val executor = TheodoliteExecutor(benchmarkExecution, benchmark) executor.run() logger.info { "Theodolite finished" } + exitProcess(0) } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/k8s/TopicManager.kt b/theodolite-quarkus/src/main/kotlin/theodolite/k8s/TopicManager.kt index 9cdf1c43fae9a72bd78126d420522b2d41a399ee..1336f57517ef74d8c781cc3b51bf130dbf8d99c5 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/k8s/TopicManager.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/k8s/TopicManager.kt @@ -4,6 +4,7 @@ import mu.KotlinLogging import org.apache.kafka.clients.admin.AdminClient import org.apache.kafka.clients.admin.ListTopicsResult import org.apache.kafka.clients.admin.NewTopic +import java.util.* private val logger = KotlinLogging.logger {} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..096d19e7c54ce3ac308ca59edee7861a7041dde0 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt @@ -0,0 +1,12 @@ +package theodolite.patcher + +import theodolite.util.PatcherDefinition +import theodolite.util.TypeName + +class PatcherDefinitionFactory { + fun createPatcherDefinition(requiredType: String, patcherTypes: List<TypeName>) : List<PatcherDefinition> { + return patcherTypes + .filter { type -> type.typeName == requiredType } + .flatMap { type -> type.patchers } + } +} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a3e56bfe7aaf01526dc8ce4ed2c106383e1f8b7 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt @@ -0,0 +1,28 @@ +package theodolite.patcher + +import io.fabric8.kubernetes.api.model.KubernetesResource +import theodolite.util.PatcherDefinition + +class PatcherFactory { + fun createPatcher(patcherDefinition: PatcherDefinition, + k8sResources: List<Pair<String, KubernetesResource>>) : Patcher { + val resource = + k8sResources.filter { it.first == patcherDefinition.resource }.map { resource -> resource.second }[0] + return when (patcherDefinition.type) { + "ReplicaPatcher" -> ReplicaPatcher(resource) + "EnvVarPatcher" -> EnvVarPatcher(resource, patcherDefinition.container, patcherDefinition.variableName) + "NodeSelectorPatcher" -> NodeSelectorPatcher(resource, patcherDefinition.variableName) + "ResourceLimitPatcher" -> ResourceLimitPatcher( + resource, + patcherDefinition.container, + patcherDefinition.variableName + ) + "ResourceRequestPatcher" -> ResourceRequestPatcher( + resource, + patcherDefinition.container, + patcherDefinition.variableName + ) + else -> throw IllegalArgumentException("Patcher type ${patcherDefinition.type} not found") + } + } +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherManager.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherManager.kt deleted file mode 100644 index 5557eb4b98d5da3bbc8b8d82227de29335c5da67..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -package theodolite.patcher - -import io.fabric8.kubernetes.api.model.KubernetesResource -import theodolite.util.PatcherDefinition -import theodolite.util.TypeName - -class PatcherManager { - private fun createK8sPatcher( - patcherDefinition: PatcherDefinition, - k8sResources: List<Pair<String, KubernetesResource>> - ): Patcher { - val resource = - k8sResources.filter { it.first == patcherDefinition.resource }.map { resource -> resource.second }[0] - return when (patcherDefinition.type) { - "ReplicaPatcher" -> ReplicaPatcher(resource) - "EnvVarPatcher" -> EnvVarPatcher(resource, patcherDefinition.container, patcherDefinition.variableName) - "NodeSelectorPatcher" -> NodeSelectorPatcher(resource, patcherDefinition.variableName) - "ResourceLimitPatcher" -> ResourceLimitPatcher( - resource, - patcherDefinition.container, - patcherDefinition.variableName - ) - "ResourceRequestPatcher" -> ResourceRequestPatcher( - resource, - patcherDefinition.container, - patcherDefinition.variableName - ) - else -> throw IllegalArgumentException("Patcher type ${patcherDefinition.type} not found") - } - } - - private fun getPatcherDef(requiredType: String, patcherTypes: List<TypeName>): List<PatcherDefinition> { - return patcherTypes - .filter { type -> type.typeName == requiredType } - .flatMap { type -> type.patchers } - } - - /** - * This function first creates a patcher definition and - * then patches the list of resources based on this patcher definition - * - * @param type Patcher type, for example "EnvVarPatcher" - * @param patcherTypes List of patcher types definitions, for example for resources and threads - * @param resources List of K8s resources, a patcher takes the resources that are needed - * @param value The value to patch - */ - fun createAndApplyPatcher( - type: String, - patcherTypes: List<TypeName>, - resources: List<Pair<String, KubernetesResource>>, - value: Any - ) { - this.getPatcherDef(type, patcherTypes) - .forEach { patcherDef -> - createK8sPatcher(patcherDef, resources).patch(value) - } - } - - /** - * Patch a resource based on the given patcher definition, a list of resources and a value to patch - * - * @param patcherDefinition The patcher definition - * @param resources List of patcher types definitions, for example for resources and threads - * @param value The value to patch - */ - fun applyPatcher( - patcherDefinition: List<PatcherDefinition>, - resources: List<Pair<String, KubernetesResource>>, - value: Any - ) { - patcherDefinition.forEach { def -> this.createK8sPatcher(def, resources).patch(value) } - } -} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/strategies/restriction/LowerBoundRestriction.kt b/theodolite-quarkus/src/main/kotlin/theodolite/strategies/restriction/LowerBoundRestriction.kt index dfd6bc8052b8ca44ac8a9220fbf1e3c8df43b93d..6fed9b5d808405b42ad374346862f050ce192141 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/strategies/restriction/LowerBoundRestriction.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/strategies/restriction/LowerBoundRestriction.kt @@ -13,7 +13,7 @@ import theodolite.util.Results class LowerBoundRestriction(results: Results) : RestrictionStrategy(results) { override fun next(load: LoadDimension, resources: List<Resource>): List<Resource> { val maxLoad: LoadDimension? = this.results.getMaxBenchmarkedLoad(load) - var lowerBound: Resource? = this.results.getMinRequiredInstances(maxLoad, resources[0].getType()) + var lowerBound: Resource? = this.results.getMinRequiredInstances(maxLoad) if (lowerBound == null) { lowerBound = resources[0] } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/LoadDimension.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/LoadDimension.kt index 29d47460bc49ec44e9a46a129e3dab3246f305b6..43cb861b2d6bbbe457a61d6f98f42487aad1d216 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/LoadDimension.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/LoadDimension.kt @@ -1,11 +1,11 @@ package theodolite.util -data class LoadDimension(private val number: Int, private val type: String) { +data class LoadDimension(private val number: Int, private val type: List<PatcherDefinition>) { fun get(): Int { return this.number } - fun getType(): String { + fun getType(): List<PatcherDefinition> { return this.type } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..e512bd7efb534c705afa3fe17dbc77396a8741db --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt @@ -0,0 +1,20 @@ +package theodolite.util + +data class PrometheusResponse( + var status: String? = null, + var data: PromData? = null +) + +data class PromData( + var resultType: String? = null, + var result: List<PromResult>? = null +) + +data class PromResult( + var metric: PromMetric? = null, + var values: List<Any>? = null +) + +data class PromMetric( + var group: String? = null +) diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/Resource.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/Resource.kt index cb172e0b8de4cff5fc08828a177f3dd9d58bbb53..094e89ebb0d4566499068331ca2fc890f3335597 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/Resource.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/Resource.kt @@ -1,11 +1,11 @@ package theodolite.util -data class Resource(private val number: Int, private val type: String) { +data class Resource(private val number: Int, private val type: List<PatcherDefinition>) { fun get(): Int { return this.number } - fun getType(): String { + fun getType(): List<PatcherDefinition> { return this.type } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/Results.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/Results.kt index 91bde71792fdca383fc9511658bab39aa58d12ce..c827e8303f5c08f4f612476a1069ecefc0a7308b 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/Results.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/Results.kt @@ -11,10 +11,10 @@ class Results { return this.results[experiment] } - fun getMinRequiredInstances(load: LoadDimension?, resourceTyp: String): Resource? { - if (this.results.isEmpty()) return Resource(Int.MIN_VALUE, resourceTyp) + fun getMinRequiredInstances(load: LoadDimension?): Resource? { + if (this.results.isEmpty()) return Resource(Int.MIN_VALUE, emptyList()) - var requiredInstances: Resource? = Resource(Int.MAX_VALUE, resourceTyp) + var requiredInstances: Resource? = Resource(Int.MAX_VALUE, emptyList()) for (experiment in results) { if (experiment.key.first == load && experiment.value) { if (requiredInstances == null) { diff --git a/theodolite-quarkus/src/main/resources/yaml/BenchmarkExecution.yaml b/theodolite-quarkus/src/main/resources/yaml/BenchmarkExecution.yaml index 270daee1708ca4791c65ff9f4a9e1a1e7e78c4d3..a91d123628a03bb7fd82821d6f34d7bf1239c154 100644 --- a/theodolite-quarkus/src/main/resources/yaml/BenchmarkExecution.yaml +++ b/theodolite-quarkus/src/main/resources/yaml/BenchmarkExecution.yaml @@ -3,17 +3,21 @@ benchmark: "benchmarkType" load: loadType: "NumSensors" loadValues: - - 10000 + - 50000 resources: resourceType: "Instances" resourceValues: - - 2 + - 1 slos: - - sloType: "slo type" + - sloType: "lag trend" threshold: 1000 + prometheusUrl: "http://localhost:32656" + externalSloUrl: "http://localhost:80/evaluate-slope" + offset: 0 + warmup: 0 execution: strategy: "LinearSearch" - duration: 300 + duration: 60 repetitions: 1 restrictions: - "LowerBound" @@ -33,7 +37,7 @@ configOverrides: resource: "uc1-kstreams-deployment.yaml" container: "uc-application" variableName: "cpu" - value: "50m" + value: "1000m" - patcher: type: "ResourceLimitPatcher" resource: "uc1-kstreams-deployment.yaml" diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt index 31eaa40fbe5424cf4df00860623e91fa5f1a9408..67c9857220a0d419183644ffaf8c6a6e16a6ce9b 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt @@ -3,6 +3,7 @@ package theodolite import io.quarkus.test.junit.QuarkusTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import theodolite.benchmark.BenchmarkExecution import theodolite.strategies.restriction.LowerBoundRestriction import theodolite.strategies.searchstrategy.BinarySearch import theodolite.strategies.searchstrategy.CompositeStrategy @@ -25,18 +26,19 @@ class CompositeStrategyTest { arrayOf(false, false, false, false, false, false, true), arrayOf(false, false, false, false, false, false, false) ) - val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, "NumSensors") } - val mockResources: List<Resource> = (0..6).map { number -> Resource(number, "Instances") } + val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, emptyList()) } + val mockResources: List<Resource> = (0..6).map { number -> Resource(number, emptyList()) } val results = Results() val benchmark = TestBenchmark() - val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results) + val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker) val linearSearch = LinearSearch(benchmarkExecutor) val lowerBoundRestriction = LowerBoundRestriction(results) val strategy = CompositeStrategy(benchmarkExecutor, linearSearch, setOf(lowerBoundRestriction)) val actual: ArrayList<Resource?> = ArrayList() - val expected: ArrayList<Resource?> = ArrayList(listOf(0, 2, 2, 3, 4, 6).map { x -> Resource(x, "Instances") }) + val expected: ArrayList<Resource?> = ArrayList(listOf(0, 2, 2, 3, 4, 6).map { x -> Resource(x, emptyList()) }) expected.add(null) for (load in mockLoads) { @@ -57,19 +59,20 @@ class CompositeStrategyTest { arrayOf(false, false, false, false, false, false, true), arrayOf(false, false, false, false, false, false, false) ) - val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, "NumSensors") } - val mockResources: List<Resource> = (0..6).map { number -> Resource(number, "Instances") } + val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, emptyList()) } + val mockResources: List<Resource> = (0..6).map { number -> Resource(number, emptyList()) } val results = Results() val benchmark = TestBenchmark() + val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() val benchmarkExecutorImpl = - TestBenchmarkExecutorImpl(mockResults, benchmark, results) + TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker) val binarySearch = BinarySearch(benchmarkExecutorImpl) val lowerBoundRestriction = LowerBoundRestriction(results) val strategy = CompositeStrategy(benchmarkExecutorImpl, binarySearch, setOf(lowerBoundRestriction)) val actual: ArrayList<Resource?> = ArrayList() - val expected: ArrayList<Resource?> = ArrayList(listOf(0, 2, 2, 3, 4, 6).map { x -> Resource(x, "Instances") }) + val expected: ArrayList<Resource?> = ArrayList(listOf(0, 2, 2, 3, 4, 6).map { x -> Resource(x, emptyList()) }) expected.add(null) for (load in mockLoads) { @@ -90,11 +93,12 @@ class CompositeStrategyTest { arrayOf(false, false, false, false, false, false, true, true), arrayOf(false, false, false, false, false, false, false, true) ) - val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, "NumSensors") } - val mockResources: List<Resource> = (0..7).map { number -> Resource(number, "Instances") } + val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, emptyList()) } + val mockResources: List<Resource> = (0..7).map { number -> Resource(number, emptyList()) } val results = Results() val benchmark = TestBenchmark() - val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results) + val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker) val binarySearch = BinarySearch(benchmarkExecutor) val lowerBoundRestriction = LowerBoundRestriction(results) val strategy = @@ -102,7 +106,7 @@ class CompositeStrategyTest { val actual: ArrayList<Resource?> = ArrayList() val expected: ArrayList<Resource?> = - ArrayList(listOf(0, 2, 2, 3, 4, 6, 7).map { x -> Resource(x, "Instances") }) + ArrayList(listOf(0, 2, 2, 3, 4, 6, 7).map { x -> Resource(x, emptyList()) }) for (load in mockLoads) { actual.add(strategy.findSuitableResource(load, mockResources)) diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt index 2170a6a54cf12433b18cc621d78a8608f3f71d63..82e4bc5d77f3f35d217c56a377513c0e7d329170 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt @@ -6,7 +6,7 @@ import io.quarkus.test.junit.QuarkusTest import io.smallrye.common.constraint.Assert.assertTrue import org.junit.jupiter.api.Test import theodolite.k8s.K8sResourceLoader -import theodolite.patcher.PatcherManager +import theodolite.patcher.PatcherFactory import theodolite.util.PatcherDefinition /** @@ -23,7 +23,7 @@ import theodolite.util.PatcherDefinition class ResourceLimitPatcherTest { val testPath = "./src/main/resources/testYaml/" val loader = K8sResourceLoader(DefaultKubernetesClient().inNamespace("")) - val manager = PatcherManager() + val patcherFactory = PatcherFactory() fun applyTest(fileName: String) { val cpuValue = "50m" @@ -42,20 +42,17 @@ class ResourceLimitPatcherTest { defMEM.container = "uc-application" defMEM.type = "ResourceLimitPatcher" - manager.applyPatcher( - patcherDefinition = listOf(defCPU), - resources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)), - value = cpuValue - ) - manager.applyPatcher( - patcherDefinition = listOf(defMEM), - resources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)), - value = memValue - ) + patcherFactory.createPatcher( + patcherDefinition = defCPU, + k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) + ).patch(value = cpuValue) + patcherFactory.createPatcher( + patcherDefinition = defMEM, + k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) + ).patch(value = memValue) k8sResource.spec.template.spec.containers.filter { it.name == defCPU.container } .forEach { - println(it) assertTrue(it.resources.limits["cpu"].toString() == cpuValue) assertTrue(it.resources.limits["memory"].toString() == memValue) } diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt index 108142843949913eb6db34bb268eab2e91fda3cf..3cd6b012f09c5471b1b011b5cd03e61a0fab1c4e 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt @@ -6,7 +6,7 @@ import io.quarkus.test.junit.QuarkusTest import io.smallrye.common.constraint.Assert.assertTrue import org.junit.jupiter.api.Test import theodolite.k8s.K8sResourceLoader -import theodolite.patcher.PatcherManager +import theodolite.patcher.PatcherFactory import theodolite.util.PatcherDefinition /** @@ -23,7 +23,7 @@ import theodolite.util.PatcherDefinition class ResourceRequestPatcherTest { val testPath = "./src/main/resources/testYaml/" val loader = K8sResourceLoader(DefaultKubernetesClient().inNamespace("")) - val manager = PatcherManager() + val patcherFactory = PatcherFactory() fun applyTest(fileName: String) { val cpuValue = "50m" @@ -42,20 +42,17 @@ class ResourceRequestPatcherTest { defMEM.container = "uc-application" defMEM.type = "ResourceRequestPatcher" - manager.applyPatcher( - patcherDefinition = listOf(defCPU), - resources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)), - value = cpuValue - ) - manager.applyPatcher( - patcherDefinition = listOf(defMEM), - resources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)), - value = memValue - ) + patcherFactory.createPatcher( + patcherDefinition = defCPU, + k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) + ).patch(value = cpuValue) + patcherFactory.createPatcher( + patcherDefinition = defMEM, + k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) + ).patch(value = memValue) k8sResource.spec.template.spec.containers.filter { it.name == defCPU.container } .forEach { - println(it) assertTrue(it.resources.requests["cpu"].toString() == cpuValue) assertTrue(it.resources.requests["memory"].toString() == memValue) } diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt index bab20b7b56a6b95efd7e616338419b632a8f42d7..2de07d48dbe59313506745e6c97b2ae9a5ed114e 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt @@ -1,6 +1,7 @@ package theodolite import theodolite.benchmark.Benchmark +import theodolite.benchmark.BenchmarkExecution import theodolite.execution.BenchmarkExecutor import theodolite.util.LoadDimension import theodolite.util.Resource @@ -10,16 +11,19 @@ import java.time.Duration class TestBenchmarkExecutorImpl( private val mockResults: Array<Array<Boolean>>, benchmark: Benchmark, - results: Results + results: Results, + slo: BenchmarkExecution.Slo ) : BenchmarkExecutor( - benchmark, results, executionDuration = Duration.ofSeconds(1), - configurationOverrides = emptyList() + benchmark, + results, + executionDuration = Duration.ofSeconds(1), + configurationOverrides = emptyList(), + slo = slo ) { override fun runExperiment(load: LoadDimension, res: Resource): Boolean { val result = this.mockResults[load.get()][res.get()] - this.results.setResult(Pair(load, res), result) return result }