From 46249fa162926112f28e29ee591110fcda0b3b26 Mon Sep 17 00:00:00 2001 From: hmitnflx <100323213+hmitnflx@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:31:51 -0700 Subject: [PATCH] Move mantis-rxcontrol from its own repo to submodule of mantis oss (#667) * Initial commit for open source. * Travis setup * Fix travis badge link. * Use `-SNAPSHOT` version for OJO publishing * Clutch now respects UserDefiend metrics, pins high first. Clutch will now always scale on a UserDefined metric if one is present in the metrics stream. It will also now pin high for the first config so as to prevent underscaling while it is learning about the environment. Also added some additional unit tests for the control loop. Finally added license headers. * Fixed some checkStyle violations. * Clutch: Default rope is now [25.0, 0.0] * Update committers list. * Added new ClutchExperimental RPS based implementation. * ClutchExperimental enters cooldown on cluster size changed. * ClutchExperimental: Heavily reduced default gain. Reduced gain by approximately two orders of magnitude but set proportional to the RPS computed by the instance. There are significant wins to be had scheduling a more approporiate gain but some thought will need to go into this process. * ClutchExperimental: Widened ROPE and decreased KP slightly. * ClutchExperimental: Can now pass a sketch -> config function if desired. * ExperimentalControlLoop now uses timer to close size observable to properly unsubscribe. * ClutchExperimental: Now respects user passed cooldown. * Updated Lombok to 1.18.12 and set ClutchConfiguration to public access level. * ClutchExperimental: Reduced gain by three orders of magnitude. * ClutchExperimental: Lag now uses derivative. * Added debug output to ExperimentalControlLoop to view autoscaler metrics as the scaler sees them. * ClutchExperimental: Now passing all sketches to configurator. * Fix bug on getting dominant resource * Add assert * Fix bug worker count reset to initial size every day * Introduce integral decay factor in PIDController * Move decay factor to be part of clutch configuration * Support decay in Integrator * add more tests * Add hooks to clutch controlloop * Make hooks easier to extend * Add clutch metric enum for source job drop * Add clutch metric enum for source job drop * Include sourcejob drop in rps computation * fix system.out * Upgrade to modern Gradle/Nebula and replace TravisCI with Github Actions * Update nebula-publish.yml * Replace JCenter with Maven Central * Add staging profile for io.mantisrx * Fix logging * Fix bug where current number of worker is incorrect * add logging * Updating publish and snapshot workflows * Update netflixoss plugin version * Add descriptive stats as a test * update github actions * fix snapshot pr target * Update nebula-publish.yml * Update nebula-ci.yml * migrate mantis-rxcontrol from github Netflix/mantis-rxcontrol as a submodule of mantis OSS * refactor package name --------- Co-authored-by: Cody Rioux Co-authored-by: Rob Spieldenner Co-authored-by: Jeff Chao Co-authored-by: Calvin Cheung Co-authored-by: calvin681 Co-authored-by: Roberto Perez Alcolea Co-authored-by: Andy Zhang <87735571+Andyz26@users.noreply.github.com> --- .../mantis-connector-job/dependencies.lock | 14 +- .../dependencies.lock | 12 +- .../dependencies.lock | 8 +- .../dependencies.lock | 38 +-- .../dependencies.lock | 103 ++++++-- .../mantis-publish-core/dependencies.lock | 18 +- .../dependencies.lock | 36 +-- .../mantis-publish-netty/dependencies.lock | 40 +-- mantis-runtime-executor/build.gradle | 6 +- mantis-runtime-executor/dependencies.lock | 85 ++++-- .../worker/jobmaster/JobAutoScaler.java | 10 +- .../MantisClutchConfigurationSelector.java | 6 +- .../rps/RpsClutchConfigurationSelector.java | 6 +- .../clutch/rps/RpsMetricComputer.java | 6 +- .../clutch/rps/RpsScaleComputer.java | 4 +- .../actuators/MantisStageActuator.java | 2 +- .../RpsClutchConfigurationSelectorTest.java | 4 +- .../clutch/rps/RpsMetricComputerTest.java | 2 +- .../clutch/rps/RpsScaleComputerTest.java | 2 +- mantis-rxcontrol/README.md | 44 ++++ mantis-rxcontrol/build.gradle | 49 ++++ mantis-rxcontrol/dependencies.lock | 203 +++++++++++++++ .../java/io/mantisrx/control/IActuator.java | 45 ++++ .../java/io/mantisrx/control/IController.java | 71 +++++ .../control/actuators/MantisJobActuator.java | 78 ++++++ .../io/mantisrx/control/clutch/Clutch.java | 100 +++++++ .../control/clutch/ClutchConfiguration.java | 59 +++++ .../control/clutch/ClutchConfigurator.java | 244 ++++++++++++++++++ .../control/clutch/ClutchExperimental.java | 126 +++++++++ .../io/mantisrx/control/clutch/Event.java | 34 +++ .../ExperimentalClutchConfigurator.java | 145 +++++++++++ .../clutch/ExperimentalControlLoop.java | 148 +++++++++++ .../control/clutch/IRpsMetricComputer.java | 32 +++ .../control/clutch/IScaleComputer.java | 32 +++ .../control/clutch/OscillationDetector.java | 76 ++++++ .../control/clutch/SymptomDetector.java | 43 +++ .../metrics/IClutchMetricsRegistry.java | 22 ++ .../control/controllers/BoostComputer.java | 34 +++ .../control/controllers/BoundToInterval.java | 42 +++ .../control/controllers/ControlLoop.java | 77 ++++++ .../control/controllers/Derivative.java | 38 +++ .../control/controllers/ErrorComputer.java | 88 +++++++ .../control/controllers/Integrator.java | 64 +++++ .../control/controllers/PIDController.java | 128 +++++++++ .../control/examples/ExampleAutoScaler.java | 89 +++++++ .../control/utils/OperatorToTransformer.java | 36 +++ .../clutch/ClutchConfigurationTest.java | 34 +++ .../clutch/ClutchConfiguratorTest.java | 73 ++++++ .../clutch/ClutchExperimentalTest.java | 22 ++ .../control/clutch/ControlLoopTest.java | 201 +++++++++++++++ .../clutch/ExperimentalControlLoopTest.java | 221 ++++++++++++++++ .../control/controllers/IntegratorTest.java | 60 +++++ .../controllers/PIDControllerTest.java | 39 +++ .../mantis-server-agent/dependencies.lock | 54 +++- .../dependencies.lock | 121 +++++++-- mantis-testcontainers/dependencies.lock | 6 +- settings.gradle | 1 + 57 files changed, 3201 insertions(+), 180 deletions(-) create mode 100644 mantis-rxcontrol/README.md create mode 100644 mantis-rxcontrol/build.gradle create mode 100644 mantis-rxcontrol/dependencies.lock create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/IActuator.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/IController.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/actuators/MantisJobActuator.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Clutch.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfiguration.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfigurator.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchExperimental.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Event.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalClutchConfigurator.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalControlLoop.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IRpsMetricComputer.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IScaleComputer.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/OscillationDetector.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/SymptomDetector.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/metrics/IClutchMetricsRegistry.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoostComputer.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoundToInterval.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ControlLoop.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Derivative.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ErrorComputer.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Integrator.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/PIDController.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/examples/ExampleAutoScaler.java create mode 100644 mantis-rxcontrol/src/main/java/io/mantisrx/control/utils/OperatorToTransformer.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfigurationTest.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfiguratorTest.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchExperimentalTest.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ControlLoopTest.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ExperimentalControlLoopTest.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/IntegratorTest.java create mode 100644 mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/PIDControllerTest.java diff --git a/mantis-connectors/mantis-connector-job/dependencies.lock b/mantis-connectors/mantis-connector-job/dependencies.lock index fb83f1d92..f4660feaf 100644 --- a/mantis-connectors/mantis-connector-job/dependencies.lock +++ b/mantis-connectors/mantis-connector-job/dependencies.lock @@ -24,7 +24,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -205,7 +205,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -394,13 +394,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ @@ -663,7 +663,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -847,13 +847,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ diff --git a/mantis-connectors/mantis-connector-publish/dependencies.lock b/mantis-connectors/mantis-connector-publish/dependencies.lock index ba5bb083a..e465408a5 100644 --- a/mantis-connectors/mantis-connector-publish/dependencies.lock +++ b/mantis-connectors/mantis-connector-publish/dependencies.lock @@ -136,7 +136,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -316,13 +316,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ @@ -543,7 +543,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -727,13 +727,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ diff --git a/mantis-examples/mantis-examples-jobconnector-sample/dependencies.lock b/mantis-examples/mantis-examples-jobconnector-sample/dependencies.lock index 47709f806..723fe94ac 100644 --- a/mantis-examples/mantis-examples-jobconnector-sample/dependencies.lock +++ b/mantis-examples/mantis-examples-jobconnector-sample/dependencies.lock @@ -255,13 +255,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ @@ -652,13 +652,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ diff --git a/mantis-examples/mantis-examples-mantis-publish-sample/dependencies.lock b/mantis-examples/mantis-examples-mantis-publish-sample/dependencies.lock index c719e97c9..f00cfa484 100644 --- a/mantis-examples/mantis-examples-mantis-publish-sample/dependencies.lock +++ b/mantis-examples/mantis-examples-mantis-publish-sample/dependencies.lock @@ -12,13 +12,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -79,13 +79,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -149,7 +149,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ @@ -157,19 +157,19 @@ "io.mantisrx:mantis-publish-netty", "io.mantisrx:mantis-publish-netty-guice" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-netty-guice" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ @@ -177,7 +177,7 @@ "io.mantisrx:mantis-publish-netty", "io.mantisrx:mantis-publish-netty-guice" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-nflx-plugin": { "firstLevelTransitive": [ @@ -269,13 +269,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -340,7 +340,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ @@ -348,19 +348,19 @@ "io.mantisrx:mantis-publish-netty", "io.mantisrx:mantis-publish-netty-guice" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-netty-guice" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ @@ -368,7 +368,7 @@ "io.mantisrx:mantis-publish-netty", "io.mantisrx:mantis-publish-netty-guice" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-nflx-plugin": { "firstLevelTransitive": [ diff --git a/mantis-examples/mantis-examples-sine-function/dependencies.lock b/mantis-examples/mantis-examples-sine-function/dependencies.lock index 470d45b88..2ba99fa06 100644 --- a/mantis-examples/mantis-examples-sine-function/dependencies.lock +++ b/mantis-examples/mantis-examples-sine-function/dependencies.lock @@ -82,6 +82,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -91,7 +97,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -265,6 +272,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -274,7 +287,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -389,11 +403,18 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common" ], - "locked": "3.0.2" + "locked": "3.0.1" + }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -418,7 +439,8 @@ }, "com.yahoo.datasketches:sketches-core": { "firstLevelTransitive": [ - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.9.1" }, @@ -490,7 +512,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" ], - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ @@ -501,7 +523,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -538,7 +561,8 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, @@ -546,10 +570,17 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.9.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "joda-time:joda-time": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -568,6 +599,12 @@ ], "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -621,7 +658,8 @@ "io.mantisrx:mantis-common", "io.mantisrx:mantis-remote-observable", "io.mantisrx:mantis-runtime", - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, @@ -723,6 +761,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -732,7 +776,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -842,11 +887,18 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common" ], - "locked": "3.0.2" + "locked": "3.0.1" + }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -871,7 +923,8 @@ }, "com.yahoo.datasketches:sketches-core": { "firstLevelTransitive": [ - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.9.1" }, @@ -943,7 +996,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" ], - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ @@ -954,7 +1007,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -991,7 +1045,8 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, @@ -999,10 +1054,17 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.9.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "joda-time:joda-time": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1021,6 +1083,12 @@ ], "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1074,7 +1142,8 @@ "io.mantisrx:mantis-common", "io.mantisrx:mantis-remote-observable", "io.mantisrx:mantis-runtime", - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, diff --git a/mantis-publish/mantis-publish-core/dependencies.lock b/mantis-publish/mantis-publish-core/dependencies.lock index aae107072..ea5654eac 100644 --- a/mantis-publish/mantis-publish-core/dependencies.lock +++ b/mantis-publish/mantis-publish-core/dependencies.lock @@ -6,7 +6,7 @@ }, "baseline-exact-dependencies-main": { "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-ext-ipc": { "locked": "0.134.0" @@ -52,10 +52,10 @@ }, "compileClasspath": { "com.netflix.archaius:archaius2-api": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "locked": "0.134.0" @@ -93,10 +93,10 @@ }, "runtimeClasspath": { "com.netflix.archaius:archaius2-api": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "locked": "0.134.0" @@ -140,10 +140,10 @@ "locked": "2.21.0" }, "com.netflix.archaius:archaius2-api": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "locked": "0.134.0" @@ -197,10 +197,10 @@ "locked": "2.21.0" }, "com.netflix.archaius:archaius2-api": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "locked": "0.134.0" diff --git a/mantis-publish/mantis-publish-netty-guice/dependencies.lock b/mantis-publish/mantis-publish-netty-guice/dependencies.lock index 710201bef..797457087 100644 --- a/mantis-publish/mantis-publish-netty-guice/dependencies.lock +++ b/mantis-publish/mantis-publish-netty-guice/dependencies.lock @@ -9,10 +9,10 @@ "locked": "4.2.2" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-ext-ipc": { "locked": "0.96.0" @@ -52,13 +52,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -110,30 +110,30 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-nflx-plugin": { "locked": "0.96.0" @@ -205,13 +205,13 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -273,30 +273,30 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-guice": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-nflx-plugin": { "locked": "0.96.0" diff --git a/mantis-publish/mantis-publish-netty/dependencies.lock b/mantis-publish/mantis-publish-netty/dependencies.lock index 53cdc78e1..a8957e0ed 100644 --- a/mantis-publish/mantis-publish-netty/dependencies.lock +++ b/mantis-publish/mantis-publish-netty/dependencies.lock @@ -9,19 +9,19 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { - "locked": "1.7.11" + "locked": "1.7.12" }, "io.mantisrx:mantis-discovery-proto": { "firstLevelTransitive": [ @@ -67,19 +67,19 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { - "locked": "1.7.11" + "locked": "1.7.12" }, "io.mantisrx:mantis-discovery-proto": { "firstLevelTransitive": [ @@ -116,25 +116,25 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "io.mantisrx:mantis-common-serde": { "firstLevelTransitive": [ @@ -190,19 +190,19 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { - "locked": "1.7.11" + "locked": "1.7.12" }, "io.mantisrx:mantis-discovery-proto": { "firstLevelTransitive": [ @@ -249,25 +249,25 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "io.mantisrx:mantis-common-serde": { "firstLevelTransitive": [ diff --git a/mantis-runtime-executor/build.gradle b/mantis-runtime-executor/build.gradle index da2bd2605..85daec82f 100644 --- a/mantis-runtime-executor/build.gradle +++ b/mantis-runtime-executor/build.gradle @@ -15,13 +15,10 @@ */ -ext { - mantisRxControlVersion = '1.3.+' -} - dependencies { api project(":mantis-runtime") api project(":mantis-runtime-loader") + api project(":mantis-rxcontrol") api project(":mantis-server:mantis-server-worker-client") implementation libraries.slf4jApi @@ -32,7 +29,6 @@ dependencies { exclude group: 'org.pentaho.pentaho-commons', module: 'pentaho-package-manager' } implementation libraries.httpClient - implementation "io.mantisrx:mantis-rxcontrol:$mantisRxControlVersion" implementation "com.yahoo.datasketches:sketches-core:0.9.1" implementation libraries.spectatorApi diff --git a/mantis-runtime-executor/dependencies.lock b/mantis-runtime-executor/dependencies.lock index fcc3866fe..882563c2a 100644 --- a/mantis-runtime-executor/dependencies.lock +++ b/mantis-runtime-executor/dependencies.lock @@ -11,9 +11,6 @@ "com.yahoo.datasketches:sketches-core": { "locked": "0.9.1" }, - "io.mantisrx:mantis-rxcontrol": { - "locked": "1.3.20" - }, "io.vavr:vavr": { "locked": "0.9.2" }, @@ -248,7 +245,7 @@ "project": true }, "io.mantisrx:mantis-rxcontrol": { - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "project": true @@ -256,7 +253,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -382,9 +380,16 @@ ], "locked": "3.0.1" }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" + }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -407,6 +412,9 @@ "locked": "0.3.1" }, "com.yahoo.datasketches:sketches-core": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], "locked": "0.9.1" }, "commons-io:commons-io": { @@ -465,7 +473,7 @@ "project": true }, "io.mantisrx:mantis-rxcontrol": { - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "project": true @@ -473,7 +481,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -510,17 +519,25 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, "io.vavr:vavr": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.9.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "joda-time:joda-time": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -536,6 +553,12 @@ "nz.ac.waikato.cms.moa:moa": { "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -585,7 +608,8 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common", "io.mantisrx:mantis-remote-observable", - "io.mantisrx:mantis-runtime" + "io.mantisrx:mantis-runtime", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, @@ -683,7 +707,7 @@ "project": true }, "io.mantisrx:mantis-rxcontrol": { - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "project": true @@ -691,7 +715,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -827,9 +852,16 @@ ], "locked": "3.0.1" }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" + }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -852,6 +884,9 @@ "locked": "0.3.1" }, "com.yahoo.datasketches:sketches-core": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], "locked": "0.9.1" }, "commons-io:commons-io": { @@ -912,7 +947,7 @@ "project": true }, "io.mantisrx:mantis-rxcontrol": { - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "project": true @@ -920,7 +955,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -957,17 +993,25 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, "io.vavr:vavr": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.9.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "io.vavr:vavr-test": { "locked": "0.9.2" }, @@ -992,6 +1036,12 @@ "nz.ac.waikato.cms.moa:moa": { "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1053,7 +1103,8 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common", "io.mantisrx:mantis-remote-observable", - "io.mantisrx:mantis-runtime" + "io.mantisrx:mantis-runtime", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, diff --git a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/JobAutoScaler.java b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/JobAutoScaler.java index 4a51b5b03..06e536a6b 100644 --- a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/JobAutoScaler.java +++ b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/JobAutoScaler.java @@ -16,10 +16,10 @@ package io.mantisrx.server.worker.jobmaster; -import com.netflix.control.clutch.Clutch; -import com.netflix.control.clutch.ClutchExperimental; import io.mantisrx.common.MantisProperties; import io.mantisrx.common.SystemParameters; +import io.mantisrx.control.clutch.Clutch; +import io.mantisrx.control.clutch.ClutchExperimental; import io.mantisrx.runtime.Context; import io.mantisrx.runtime.descriptor.SchedulingInfo; import io.mantisrx.runtime.descriptor.StageScalingPolicy; @@ -104,9 +104,9 @@ Observer getObserver() { return new SerializedObserver<>(subject); } - private com.netflix.control.clutch.Event mantisEventToClutchEvent(Event event) { + private io.mantisrx.control.clutch.Event mantisEventToClutchEvent(Event event) { logger.debug("Converting Mantis event to Clutch event: {}", event); - return new com.netflix.control.clutch.Event(metricMap.get(event.type), event.getEffectiveValue()); + return new io.mantisrx.control.clutch.Event(metricMap.get(event.type), event.getEffectiveValue()); } void start() { @@ -186,7 +186,7 @@ void start() { StageScaler scaler = new StageScaler(stage, stageSchedulingInfo); MantisStageActuator actuator = new MantisStageActuator(initialSize, scaler); - Observable.Transformer transformToClutchEvent = + Observable.Transformer transformToClutchEvent = obs -> obs.map(event -> this.mantisEventToClutchEvent(event)) .filter(event -> event.metric != null); Observable workerCounts = context.getWorkerMapObservable() diff --git a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/experimental/MantisClutchConfigurationSelector.java b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/experimental/MantisClutchConfigurationSelector.java index 308dc8794..26d5ae590 100644 --- a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/experimental/MantisClutchConfigurationSelector.java +++ b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/experimental/MantisClutchConfigurationSelector.java @@ -16,9 +16,9 @@ package io.mantisrx.server.worker.jobmaster.clutch.experimental; -import com.netflix.control.clutch.Clutch; -import com.netflix.control.clutch.ClutchConfiguration; import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.clutch.Clutch; +import io.mantisrx.control.clutch.ClutchConfiguration; import io.mantisrx.runtime.descriptor.StageSchedulingInfo; import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; import io.vavr.Function1; @@ -111,7 +111,7 @@ public ClutchConfiguration apply(Map sketche // TODO: Do we want to reset sketches, we need at least one day's values //resetSketches(sketches); - return com.netflix.control.clutch.ClutchConfiguration.builder() + return ClutchConfiguration.builder() .metric(Clutch.Metric.RPS) .setPoint(setPoint) .kp(kp) diff --git a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelector.java b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelector.java index 58c505eeb..69b4dc0f6 100644 --- a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelector.java +++ b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelector.java @@ -16,9 +16,9 @@ package io.mantisrx.server.worker.jobmaster.clutch.rps; -import com.netflix.control.clutch.Clutch; -import com.netflix.control.clutch.ClutchConfiguration; import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.clutch.Clutch; +import io.mantisrx.control.clutch.ClutchConfiguration; import io.mantisrx.runtime.descriptor.StageSchedulingInfo; import io.vavr.Function1; import io.vavr.Tuple2; @@ -51,7 +51,7 @@ public ClutchConfiguration apply(Map sketche double ki = 0.0; double kd = 1.0 / Math.max(setPoint, 1.0) / Math.max(getCumulativeIntegralDivisor(getIntegralScaler(), deltaT), 1.0); - ClutchConfiguration config = com.netflix.control.clutch.ClutchConfiguration.builder() + ClutchConfiguration config = ClutchConfiguration.builder() .metric(Clutch.Metric.RPS) .setPoint(setPoint) .kp(kp) diff --git a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputer.java b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputer.java index d105a94a7..7809b0be4 100644 --- a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputer.java +++ b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputer.java @@ -16,9 +16,9 @@ package io.mantisrx.server.worker.jobmaster.clutch.rps; -import com.netflix.control.clutch.Clutch; -import com.netflix.control.clutch.ClutchConfiguration; -import com.netflix.control.clutch.IRpsMetricComputer; +import io.mantisrx.control.clutch.Clutch; +import io.mantisrx.control.clutch.ClutchConfiguration; +import io.mantisrx.control.clutch.IRpsMetricComputer; import java.util.Map; public class RpsMetricComputer implements IRpsMetricComputer { diff --git a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputer.java b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputer.java index 8ae119078..efc02a64c 100644 --- a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputer.java +++ b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputer.java @@ -15,8 +15,8 @@ */ package io.mantisrx.server.worker.jobmaster.clutch.rps; -import com.netflix.control.clutch.ClutchConfiguration; -import com.netflix.control.clutch.IScaleComputer; +import io.mantisrx.control.clutch.ClutchConfiguration; +import io.mantisrx.control.clutch.IScaleComputer; public class RpsScaleComputer implements IScaleComputer { private final ClutchRpsPIDConfig rpsConfig; diff --git a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/control/actuators/MantisStageActuator.java b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/control/actuators/MantisStageActuator.java index 62968c439..f3991dc02 100644 --- a/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/control/actuators/MantisStageActuator.java +++ b/mantis-runtime-executor/src/main/java/io/mantisrx/server/worker/jobmaster/control/actuators/MantisStageActuator.java @@ -16,7 +16,7 @@ package io.mantisrx.server.worker.jobmaster.control.actuators; -import com.netflix.control.IActuator; +import io.mantisrx.control.IActuator; import io.mantisrx.server.worker.jobmaster.JobAutoScaler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelectorTest.java b/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelectorTest.java index 195d35d30..4bff90487 100644 --- a/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelectorTest.java +++ b/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsClutchConfigurationSelectorTest.java @@ -19,9 +19,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import com.netflix.control.clutch.Clutch; -import com.netflix.control.clutch.ClutchConfiguration; import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.clutch.Clutch; +import io.mantisrx.control.clutch.ClutchConfiguration; import io.mantisrx.runtime.descriptor.StageScalingPolicy; import io.mantisrx.runtime.descriptor.StageSchedulingInfo; import io.mantisrx.shaded.com.google.common.collect.ImmutableMap; diff --git a/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputerTest.java b/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputerTest.java index 1f07ae5e7..1ac8a027b 100644 --- a/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputerTest.java +++ b/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsMetricComputerTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.assertEquals; -import com.netflix.control.clutch.Clutch; +import io.mantisrx.control.clutch.Clutch; import io.mantisrx.shaded.com.google.common.collect.ImmutableMap; import java.util.Map; import org.junit.Test; diff --git a/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputerTest.java b/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputerTest.java index 74297500e..d47621759 100644 --- a/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputerTest.java +++ b/mantis-runtime-executor/src/test/java/io/mantisrx/server/worker/jobmaster/clutch/rps/RpsScaleComputerTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.assertEquals; -import com.netflix.control.clutch.ClutchConfiguration; +import io.mantisrx.control.clutch.ClutchConfiguration; import io.vavr.Tuple; import io.vavr.control.Option; import org.junit.Test; diff --git a/mantis-rxcontrol/README.md b/mantis-rxcontrol/README.md new file mode 100644 index 000000000..2a573755e --- /dev/null +++ b/mantis-rxcontrol/README.md @@ -0,0 +1,44 @@ +# mantis-rxcontrol + +## Design + +### PID + +### Clutch +Clutch is an autoscaling domain specific implementation for scaling +stateless systems. Initially Clutch was designed to autoscale Mantis +Kafka source jobs + +#### Metric +`Metric` is an enum contained within the `Clutch` class allowing various +systems to map their resource measurements into something Clutch understands. + +#### Event +A pair of `Metric` and `Double` representing a measurement taken from the +target system. For example `new Event(Metric.CPU, 78.2)` represents a measurement +of 78.2% CPU usage. The Clutch systems will operate on an `Observable` +stream. + +#### ClutchConfigurator +The ClutchConfigurator class within the `io.mantisrx.control.clutch` namespace is +responsible for consuming an `Observable` and producing an appropriate +`ClutchConfiguration` instance for the taget system. Currently the configurator +performs the following tasks; + +* Determines the appropriate metric for autoscaling from the set CPU, Memory, Network. +* Determines the actual range of resource usage and autoscales using those as min/max. + +In the near future this class will also be responsible for; + +* Adjusting configuration based on lag/drops. +* Adjusting configuration based on oscillation. + +#### Clutch +`Clutch` contained within the `io.mantisrx.control.Clutch` namespace is a wrapper +around a `PID` controller containing domain specific knowledge / implementations +for the task of autoscaling stateless systems. This class is currently responsible +for; + +* Instantiating a ClutchConfigurator. +* Attaching a ControlLoop which executes the autoscaling. + diff --git a/mantis-rxcontrol/build.gradle b/mantis-rxcontrol/build.gradle new file mode 100644 index 000000000..39da3b08c --- /dev/null +++ b/mantis-rxcontrol/build.gradle @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +ext { + unirestVersion = '1.4.9' + sketchesVersion = '0.9.1' + rxJavaMathVersion = '0.20.6' + assertjVersion = '3.12.+' +} + +dependencies { + api libraries.mantisShaded + + implementation libraries.commonsMath3 + implementation libraries.rxJava + implementation libraries.slf4jApi + implementation libraries.vavr + implementation libraries.vavrJackson + + implementation "com.mashape.unirest:unirest-java:$unirestVersion" + implementation("com.netflix.rxjava:rxjava-math:$rxJavaMathVersion") { + exclude module: 'rxjava-core' + } + implementation "com.yahoo.datasketches:sketches-core:$sketchesVersion" + + testImplementation libraries.junit4 + testImplementation libraries.mockitoCore + testImplementation "org.assertj:assertj-core:$assertjVersion" +} + +task runScaler(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'io.mantisrx.control.ExperimentRunner' +} + +//apply from: file('gradle/check.gradle') diff --git a/mantis-rxcontrol/dependencies.lock b/mantis-rxcontrol/dependencies.lock new file mode 100644 index 000000000..dfeaacb45 --- /dev/null +++ b/mantis-rxcontrol/dependencies.lock @@ -0,0 +1,203 @@ +{ + "annotationProcessor": { + "org.projectlombok:lombok": { + "locked": "1.18.20" + } + }, + "baseline-exact-dependencies-main": { + "com.mashape.unirest:unirest-java": { + "locked": "1.4.9" + }, + "com.netflix.rxjava:rxjava-math": { + "locked": "0.20.6" + }, + "com.yahoo.datasketches:sketches-core": { + "locked": "0.9.1" + }, + "io.reactivex:rxjava": { + "locked": "1.3.8" + }, + "io.vavr:vavr": { + "locked": "0.9.2" + }, + "org.apache.commons:commons-math3": { + "locked": "3.5" + }, + "org.slf4j:slf4j-api": { + "locked": "1.7.0" + } + }, + "baseline-exact-dependencies-test": { + "junit:junit": { + "locked": "4.11" + }, + "junit:junit-dep": { + "locked": "4.11" + }, + "org.assertj:assertj-core": { + "locked": "3.12.2" + }, + "org.mockito:mockito-core": { + "locked": "2.0.111-beta" + } + }, + "compileClasspath": { + "com.mashape.unirest:unirest-java": { + "locked": "1.4.9" + }, + "com.netflix.rxjava:rxjava-math": { + "locked": "0.20.6" + }, + "com.yahoo.datasketches:sketches-core": { + "locked": "0.9.1" + }, + "io.mantisrx:mantis-shaded": { + "project": true + }, + "io.reactivex:rxjava": { + "locked": "1.3.8" + }, + "io.vavr:vavr": { + "locked": "0.9.2" + }, + "org.apache.commons:commons-math3": { + "locked": "3.5" + }, + "org.projectlombok:lombok": { + "locked": "1.18.20" + }, + "org.slf4j:slf4j-api": { + "locked": "1.7.0" + } + }, + "lombok": { + "org.projectlombok:lombok": { + "locked": "1.18.20" + } + }, + "runtimeClasspath": { + "com.mashape.unirest:unirest-java": { + "locked": "1.4.9" + }, + "com.netflix.rxjava:rxjava-math": { + "locked": "0.20.6" + }, + "com.yahoo.datasketches:sketches-core": { + "locked": "0.9.1" + }, + "io.mantisrx:mantis-shaded": { + "project": true + }, + "io.reactivex:rxjava": { + "locked": "1.3.8" + }, + "io.vavr:vavr": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-shaded" + ], + "locked": "0.9.2" + }, + "io.vavr:vavr-jackson": { + "locked": "0.9.2" + }, + "org.apache.commons:commons-math3": { + "locked": "3.5" + }, + "org.slf4j:slf4j-api": { + "locked": "1.7.0" + } + }, + "testAnnotationProcessor": { + "org.projectlombok:lombok": { + "locked": "1.18.20" + } + }, + "testCompileClasspath": { + "com.mashape.unirest:unirest-java": { + "locked": "1.4.9" + }, + "com.netflix.rxjava:rxjava-math": { + "locked": "0.20.6" + }, + "com.yahoo.datasketches:sketches-core": { + "locked": "0.9.1" + }, + "io.mantisrx:mantis-shaded": { + "project": true + }, + "io.reactivex:rxjava": { + "locked": "1.3.8" + }, + "io.vavr:vavr": { + "locked": "0.9.2" + }, + "io.vavr:vavr-jackson": { + "locked": "0.9.2" + }, + "junit:junit": { + "locked": "4.11" + }, + "junit:junit-dep": { + "locked": "4.11" + }, + "org.apache.commons:commons-math3": { + "locked": "3.5" + }, + "org.assertj:assertj-core": { + "locked": "3.12.2" + }, + "org.mockito:mockito-core": { + "locked": "2.0.111-beta" + }, + "org.projectlombok:lombok": { + "locked": "1.18.20" + }, + "org.slf4j:slf4j-api": { + "locked": "1.7.0" + } + }, + "testRuntimeClasspath": { + "com.mashape.unirest:unirest-java": { + "locked": "1.4.9" + }, + "com.netflix.rxjava:rxjava-math": { + "locked": "0.20.6" + }, + "com.yahoo.datasketches:sketches-core": { + "locked": "0.9.1" + }, + "io.mantisrx:mantis-shaded": { + "project": true + }, + "io.reactivex:rxjava": { + "locked": "1.3.8" + }, + "io.vavr:vavr": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-shaded" + ], + "locked": "0.9.2" + }, + "io.vavr:vavr-jackson": { + "locked": "0.9.2" + }, + "junit:junit": { + "locked": "4.11" + }, + "junit:junit-dep": { + "locked": "4.11" + }, + "org.apache.commons:commons-math3": { + "locked": "3.5" + }, + "org.assertj:assertj-core": { + "locked": "3.12.2" + }, + "org.mockito:mockito-core": { + "locked": "2.0.111-beta" + }, + "org.slf4j:slf4j-api": { + "locked": "1.7.0" + } + } +} \ No newline at end of file diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/IActuator.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/IActuator.java new file mode 100644 index 000000000..9a05ec964 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/IActuator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control; + +import rx.functions.Func1; + +/** + * Same interface as IController but separated so that actuators can be identified. + * + * The job of the actuator is to receive a size and alter the size of the target + * to be autoscaled. This might for example be a Mantis Job, a Flink Router or even + * an AWS Autoscaling Group. + */ +public abstract class IActuator extends IController { + + /** + * Static factory method for constructing an instance of IAcuator. + * + * @param fn A function which presumably side-effects for actuation. Should return its input. + * @return An IActuator which calls fn with the value. + */ + public static IActuator of(Func1 fn) { + + return new IActuator() { + @Override + protected Double processStep(Double value) { + return fn.call(value); + } + }; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/IController.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/IController.java new file mode 100644 index 000000000..007aa715c --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/IController.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control; + +import rx.Observable; +import rx.Subscriber; + +/** + * The Feedback Principle: Constantly compare the actual output to the + * setpoint; then apply a corrective action in the proper direction and + * approximately of the correct size. + * + * Iteratively applying changes in the correct direction allows this + * system to converge onto the correct value over time. + * + */ +public abstract class IController implements Observable.Operator { + + private final IController parent = this; + + /** + * Implementation method for Controller components. Surrounding RxJava machinery will call this method. + * + * @param value Input from previous stage of control loop processing. + * @return Output intended for next stage of control loop processing. + */ + protected abstract Double processStep(Double value); + + + @Override + public Subscriber call(final Subscriber s) { + + return new Subscriber(s) { + @Override + public void onCompleted() { + if (!s.isUnsubscribed()) { + s.onCompleted(); + } + } + + @Override + public void onError(Throwable t) { + if (!s.isUnsubscribed()) { + s.onError(t); + } + } + + @Override + public void onNext(Double error) { + Double controlAction = parent.processStep(error); + if (!s.isUnsubscribed()) { + s.onNext(controlAction); + } + } + }; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/actuators/MantisJobActuator.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/actuators/MantisJobActuator.java new file mode 100644 index 000000000..b9905a29b --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/actuators/MantisJobActuator.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.actuators; + +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.Unirest; +import io.mantisrx.control.IActuator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Actuator which controls the number of instances for a particular job id and stage. + */ +public class MantisJobActuator extends IActuator { + + private final String jobId; + private final Integer stageNumber; + private static Logger logger = LoggerFactory.getLogger(MantisJobActuator.class); + + private Long lastValue = Long.MIN_VALUE; + + private final String url; + + public MantisJobActuator(String jobId, Integer stageNumber, String environ, String region, String stack) { + this.jobId = jobId; + this.stageNumber = stageNumber; + + this.url = "staging".equals(stack.toLowerCase()) + ? "https://mantisapi.staging." + region + "." + environ + ".netflix.net" + : "https://mantisapi." + region + "." + environ + ".netflix.net"; + + logger.debug("Using scaling endpoint: " + url); + } + + + private final String scaleEndPoint = "/api/jobs/scaleStage"; + + + + + @Override + protected Double processStep(Double input) { + Long numWorkers = ((Double) Math.ceil(input)).longValue(); + + if (numWorkers != lastValue) { + logger.info("Scaling " + this.jobId + " to " + numWorkers + " workers."); + + String payload = "{\"JobId\":\"" + this.jobId + "\",\"StageNumber\":" + + this.stageNumber + ",\"NumWorkers\":\"" + numWorkers + "\"}"; + + try { + HttpResponse resp = Unirest.post(url + scaleEndPoint) + .header("accept", "application/json") + .body(payload) + .asString(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + + lastValue = numWorkers; + } + return numWorkers * 1.0; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Clutch.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Clutch.java new file mode 100644 index 000000000..29c3f3bf5 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Clutch.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import io.mantisrx.control.IActuator; +import io.mantisrx.control.clutch.metrics.IClutchMetricsRegistry; +import io.mantisrx.control.controllers.ControlLoop; +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import rx.Observable; + +/** + * Clutch is our domain specific autoscaler which adopts many elements from control theory but allows us to fully + * encapsulate our desired autoscaling behavior. + * + * - Multiple Metric handled automatically identifying the dominant metric. + * - Handles dampening to prevent oscillation. + * - Handles a resistance metric if users want to provide feedback to the scaler itself. + **/ +public class Clutch implements Observable.Transformer { + + /** Specifies all the Metrics clutch is capable of dealing with. */ + public enum Metric { + /** CPU Resource Metric. */ + CPU, + /** Memory Resource Metric. */ + MEMORY, + /** Network Resource Metric. */ + NETWORK, + /** Messages left unpolled, typically Kafka lag. */ + LAG, + /** Messages dropped. */ + DROPS, + /** Hypothetical metric which causes the controller to slow down. Currently unused. */ + RESISTANCE, + /** A user defined resource metric provided by the job under control. Receives priority. */ + UserDefined, + /** A measure of requests per second handled by the target. */ + RPS, + /** Messages dropped by by the source job when sending to current job. */ + SOURCEJOB_DROP + } + + private final IActuator actuator; + private final AtomicLong initialSize; + private final Integer minSize; + private final Integer maxSize; + private final AtomicDouble dampener; + private Integer loggingIntervalMins = 60; + private final Observable timer = Observable.interval(1, TimeUnit.DAYS).share(); + + /** + * Constructs a new Clutch instance for autoscaling. + * @param actuator A function of Double -> Double which causes the scaling to occur. + * @param initialSize The initial size of the cluster as it exists before scaling. + * @param minSize The minimum size to which the cluster can/should scale. + * @param maxSize The maximum size to which the cluster can/shoulds cale. + */ + public Clutch(IActuator actuator, Integer initialSize, Integer minSize, Integer maxSize) { + this.actuator = actuator; + this.initialSize = new AtomicLong(initialSize); + this.minSize = minSize; + this.maxSize = maxSize; + this.dampener = new AtomicDouble(1.0); + } + + public Clutch(IActuator actuator, Integer initialSize, + Integer minSize, Integer maxSize, Integer loggingIntervalMins) { + this(actuator, initialSize, minSize, maxSize); + this.loggingIntervalMins = loggingIntervalMins; + } + + @Override + public Observable call(Observable eventObservable) { + final Observable events = eventObservable.share(); + + return events + .compose(new ClutchConfigurator(new IClutchMetricsRegistry() { }, minSize, + maxSize, timer, this.loggingIntervalMins)) + .flatMap(config -> events.compose(new ControlLoop(config, this.actuator, + this.initialSize, dampener)) + .takeUntil(timer)) // takeUntil tears down this control loop when a new config is produced. + .lift(new OscillationDetector(60, x -> this.dampener.set(Math.pow(x, 3)))); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfiguration.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfiguration.java new file mode 100644 index 000000000..ceb601e7d --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import io.vavr.Tuple2; +import java.util.concurrent.TimeUnit; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Value; + +/** + * Represents the overall configuration of a Clutch control loop. + */ +@EqualsAndHashCode +public @Builder(access = AccessLevel.PUBLIC) @Value class ClutchConfiguration { + + /** The Metric for which this configuration is intended. */ + public final Clutch.Metric metric; + /** The setPoint will be the value for the metric tracked by the controller. */ + public final double setPoint; + + /** Proportional controller gain. */ + public final double kp; + /** Integral controller gain. */ + public final double ki; + /** Derivative controller gain. */ + public final double kd; + /** Integral component decay factor. */ + @Builder.Default + public final double integralDecay = 1.0; + + /** Minimum size for autoscaling. */ + public final int minSize; + /** Maximum size for autoscaling */ + public final int maxSize; + + /** Region of Practical Equivalence. Value below and above setPoint which is treated as equal to the setPoint. */ + public final Tuple2 rope; + + /** Cooldown interval for the autoscaler. */ + public final long cooldownInterval; + /** Cooldown time units for the autoscaler. */ + public final TimeUnit cooldownUnits; +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfigurator.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfigurator.java new file mode 100644 index 000000000..634e48240 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchConfigurator.java @@ -0,0 +1,244 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import com.yahoo.sketches.quantiles.DoublesSketch; +import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.clutch.metrics.IClutchMetricsRegistry; +import io.mantisrx.shaded.com.google.common.annotations.VisibleForTesting; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import rx.Observable; +import rx.schedulers.Schedulers; + + +/** + * The ClutchConfigurator's responsibility is to Observe the metrics stream for the workers + * in a single stage and recommend a configuration for the autoscaler. + * + * There are several responsibilities; + * - Determine the dominant metric and recommend scaling occur on this metric. + * - Determine the true maximum achievable value for a metric and instead scale on that. + * + * WHAT ELSE? + * - Determine if a job is overprovisioned / underprovisioned. + * - What can we do with lag and drops? + * - What can we do with oscillation? + * - What can we do if maxSize is too small? + * + */ +@Slf4j +public class ClutchConfigurator implements Observable.Transformer { + + private static double DEFAULT_SETPOINT = 60.0; + private static Tuple2 DEFAULT_ROPE = Tuple.of(25.0, 0.00); + private static int DEFAULT_K = 1024; + private static double DEFAULT_QUANTILE = 0.99; + + private IClutchMetricsRegistry metricsRegistry; + private final Integer minSize; + private final Integer maxSize; + private final Observable timer; + private Integer loggingIntervalMins = 60; + + /** Metrics which represent a resources and are consequently usable for scaling. */ + private static Set resourceMetrics = HashSet + .of(Clutch.Metric.CPU, Clutch.Metric.MEMORY, Clutch.Metric.NETWORK, Clutch.Metric.UserDefined); + + private static ConcurrentHashMap sketches = new ConcurrentHashMap<>(); + static { + sketches.put(Clutch.Metric.CPU, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.MEMORY, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.NETWORK, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.LAG, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.DROPS, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.UserDefined, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + } + + public ClutchConfigurator(IClutchMetricsRegistry metricsRegistry, Integer minSize, Integer maxSize, Observable timer) { + this.metricsRegistry = metricsRegistry; + this.minSize = minSize; + this.maxSize = maxSize; + this.timer = timer; + } + + public ClutchConfigurator(IClutchMetricsRegistry metricsRegistry, Integer minSize, Integer maxSize, Observable timer, + Integer loggingIntervalMins) { + this(metricsRegistry, minSize, maxSize, timer); + this.loggingIntervalMins = loggingIntervalMins; + } + + // + // Metrics + // + + /** + * Determines the dominant metric given a stream of Metric -> UpdateDoublesSketch. + * If a User Defined metric is present we will always use it. + * @param metrics + * @return A Clutch.Metric on which the job should scale. + */ + private static Clutch.Metric determineDominantMetric(List> metrics) { + if (metrics.stream().filter(metric -> metric.getKey() == Clutch.Metric.UserDefined).count() > 0) { + return Clutch.Metric.UserDefined; + } + + Clutch.Metric metric = metrics.stream() + .max(Comparator.comparingDouble(a -> a.getValue().getQuantile(DEFAULT_QUANTILE))) + .map(Map.Entry::getKey) + .get(); + log.info("Determined dominant resource: {}", metric.toString()); + return metric; + } + + + /** + * The objective is to determine a setpoint which takes into account the fact that + * the worker may not be able to use all of the provisioned resources. + * + * @param metric A DoublesSketch representing the metric in question. + * @return An appropriate setpoint for a controller to use for autoscaling. + */ + private static double determineSetpoint(DoublesSketch metric) { + double quantile = metric.getQuantile(DEFAULT_QUANTILE); + double setPoint = quantile * (DEFAULT_SETPOINT / 100.0); + setPoint = setPoint == Double.NaN ? DEFAULT_SETPOINT : setPoint; + double bounded = bound(1.0, DEFAULT_SETPOINT, setPoint); + log.info("Determined quantile {} and setPoint of {} bounding to {}.", quantile, setPoint, bounded); + return bounded; + } + + // + // Configs + // + + /** + * Generates a configuration based on Clutch's best understanding of the job at this time. + * @return A configuration suitable for autoscaling with Clutch. + */ + protected ClutchConfiguration getConfig() { + Clutch.Metric dominantResource = determineDominantMetric(sketches.entrySet().stream() + .filter(x -> isResourceMetric(x.getKey())) + .filter(x -> x.getValue().getN() > 0) + .collect(Collectors.toList())); + + double setPoint = determineSetpoint(sketches.get(dominantResource)); + + return new ClutchConfiguration.ClutchConfigurationBuilder() + .metric(dominantResource) + .setPoint(setPoint) + .kp(0.01) + .ki(0.01) + .kd(0.01) + .minSize(this.minSize) + .maxSize(this.maxSize) + .rope(DEFAULT_ROPE) + .cooldownInterval(5) + .cooldownUnits(TimeUnit.MINUTES) + .build(); + } + + /** + * Generates a configuration whose purpose is to pin high. + * @return A config which simply pins the controller to the maximum value. + */ + private ClutchConfiguration getPinHighConfig() { + return new ClutchConfiguration.ClutchConfigurationBuilder() + .metric(Clutch.Metric.CPU) + .setPoint(DEFAULT_SETPOINT) + .kp(0.01) + .ki(0.01) + .kd(0.01) + .minSize(this.maxSize) + .maxSize(this.maxSize) + .rope(DEFAULT_ROPE) + .cooldownInterval(5) + .cooldownUnits(TimeUnit.MINUTES) + .build(); + } + + protected UpdateDoublesSketch getSketch(Clutch.Metric metric) { + return sketches.get(metric); + } + + @Override + public Observable call(Observable eventObservable) { + + eventObservable = eventObservable.share(); + + Observable logs = Observable.interval(this.loggingIntervalMins, TimeUnit.MINUTES) + .observeOn(Schedulers.newThread()) + .map(__ -> { + logSketchSummary("CPU", sketches.get(Clutch.Metric.CPU)); + logSketchSummary("MEMORY", sketches.get(Clutch.Metric.MEMORY)); + logSketchSummary("NETWORK", sketches.get(Clutch.Metric.NETWORK)); + logSketchSummary("UserDefined", sketches.get(Clutch.Metric.UserDefined)); + return null; + }); + + Observable configs = timer + .map(__ -> getConfig()); + + return eventObservable + .filter(event -> event != null && event.metric != null) + .map(event -> { + UpdateDoublesSketch sketch = sketches.computeIfAbsent(event.metric, metric -> + UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketch.update(event.value); + return null; + }) // Encourages RxJava to actually consume events. + .mergeWith(logs) // Encourages RxJava to actually consume events. + .filter(Objects::nonNull) + .cast(ClutchConfiguration.class) + .mergeWith(Observable.just(getPinHighConfig())) // Initial config + .mergeWith(configs) // Stream of configs. + .doOnNext(config -> log.info(config.toString())); + } + + + // + // Utils + // + + private void logSketchSummary(String name, UpdateDoublesSketch sketch) { + log.info("{} sketch ({}) min: {}, max: {}, median: {}, 99th: {}", name, sketch.getN(), sketch.getMinValue(), sketch.getMaxValue(), sketch.getQuantile(0.5), sketch.getQuantile(0.99)); + } + + + private static boolean isResourceMetric(Clutch.Metric metric) { + return resourceMetrics.contains(metric); + } + + @VisibleForTesting + static double bound(double min, double max, double value) { + return value < min + ? min + : value > max + ? max + : value; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchExperimental.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchExperimental.java new file mode 100644 index 000000000..a8d42080a --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ClutchExperimental.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.IActuator; +import io.mantisrx.control.clutch.metrics.IClutchMetricsRegistry; +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; +import io.vavr.Function1; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import rx.Observable; + +/** + * Clutch experimental is taking a radically different approach to auto scaling, something akin to the first + * iteration of clutch but with lessons from the past year of auto scaling. + */ +public class ClutchExperimental implements Observable.Transformer { + + private final IActuator actuator; + private final AtomicLong currentSize; + private final Integer minSize; + private final Integer maxSize; + + private final Observable timer; + private final Observable sizeObs; + private final long initialConfigMillis; + private final Function1, ClutchConfiguration> configurator; + private final IRpsMetricComputer rpsMetricComputer; + private final IScaleComputer scaleComputer; + + /** + * Constructs a new Clutch instance for autoscaling. + * @param actuator A function of Double -> Double which causes the scaling to occur. + * @param initialSize The initial size of the cluster as it exists before scaling. + * @param minSize The minimum size to which the cluster can/should scale. + * @param maxSize The maximum size to which the cluster can/should scale. + * @param sizeObs An observable indicating the size of the cluster should external events resize it. + * @param timer An observable on which each tick signifies a new configuration should be emitted. + * @param initialConfigMillis The initial number of milliseconds before initial configuration. + * @param configurator Function to generate a ClutchConfiguration based on metric sketches. + * @param rpsMetricComputer Computes the RPS metric to be feed into the PID controller. + * @param scaleComputer Computes the new scale based on the current scale and PID controller output. + * + */ + public ClutchExperimental(IActuator actuator, Integer initialSize, Integer minSize, Integer maxSize, + Observable sizeObs, Observable timer, long initialConfigMillis, + Function1, ClutchConfiguration> configurator, + IRpsMetricComputer rpsMetricComputer, + IScaleComputer scaleComputer) { + this.actuator = actuator; + this.currentSize = new AtomicLong(initialSize); + this.minSize = minSize; + this.maxSize = maxSize; + this.sizeObs = sizeObs; + this.timer = timer; + this.initialConfigMillis = initialConfigMillis; + this.configurator = configurator; + this.rpsMetricComputer = rpsMetricComputer; + this.scaleComputer = scaleComputer; + } + + public ClutchExperimental(IActuator actuator, Integer currentSize, Integer minSize, Integer maxSize, + Observable sizeObs, Observable timer, long initialConfigMillis, + Function1, ClutchConfiguration> configurator) { + this(actuator, currentSize, minSize, maxSize, sizeObs, timer, initialConfigMillis, configurator, + new ExperimentalControlLoop.DefaultRpsMetricComputer(), new ExperimentalControlLoop.DefaultScaleComputer()); + } + + public ClutchExperimental(IActuator actuator, Integer currentSize, Integer minSize, Integer maxSize, + Observable sizeObs, Observable timer, long initialConfigMillis, long coolDownSeconds) { + + this(actuator, currentSize, minSize, maxSize, sizeObs, timer, initialConfigMillis, (sketches) -> { + double setPoint = 0.6 * sketches.get(Clutch.Metric.RPS).getQuantile(0.99); + Tuple2 rope = Tuple.of(setPoint * 0.15, 0.0); + + // TODO: Significant improvements to gain computation can likely be made. + double kp = (setPoint * 1e-9) / 5.0; + double ki = 0.0; + double kd = (setPoint * 1e-9) / 4.0; + + return new ClutchConfiguration.ClutchConfigurationBuilder() + .metric(Clutch.Metric.RPS) + .setPoint(setPoint) + .kp(kp) + .ki(ki) + .kd(kd) + .minSize(minSize) + .maxSize(maxSize) + .rope(rope) + .cooldownInterval(coolDownSeconds) + .cooldownUnits(TimeUnit.SECONDS) + .build(); + }); + } + + @Override + public Observable call(Observable eventObservable) { + final Observable events = eventObservable.share(); + + return events + .compose(new ExperimentalClutchConfigurator(new IClutchMetricsRegistry() { }, timer, + initialConfigMillis, configurator)) + .switchMap(config -> events + .compose(new ExperimentalControlLoop(config, this.actuator, + this.currentSize, new AtomicDouble(1.0), timer, sizeObs, + this.rpsMetricComputer, this.scaleComputer))); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Event.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Event.java new file mode 100644 index 000000000..99c20e75f --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/Event.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +public class Event { + public final Clutch.Metric metric; + public final double value; + public Event(Clutch.Metric metric, double value) { + this.metric = metric; + this.value = value; + } + + public Clutch.Metric getMetric() { + return metric; + } + + public double getValue() { + return value; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalClutchConfigurator.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalClutchConfigurator.java new file mode 100644 index 000000000..0c0d29dc3 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalClutchConfigurator.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import com.yahoo.sketches.quantiles.DoublesSketch; +import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.clutch.metrics.IClutchMetricsRegistry; +import io.vavr.Function1; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import rx.Observable; + +@Slf4j +public class ExperimentalClutchConfigurator implements Observable.Transformer { + private static int DEFAULT_K = 1024; + + private static int NUM_STATS_DATA_POINTS = (int) TimeUnit.DAYS.toMinutes(7) * 2; + private IClutchMetricsRegistry metricsRegistry; + private final Observable timer; + private final long initialConfigMilis; + private final Function1, ClutchConfiguration> configurator; + + private static ConcurrentHashMap sketches = new ConcurrentHashMap<>(); + private static ConcurrentHashMap stats = new ConcurrentHashMap<>(); + static { + sketches.put(Clutch.Metric.CPU, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.MEMORY, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.NETWORK, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.LAG, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.DROPS, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.UserDefined, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.RPS, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketches.put(Clutch.Metric.SOURCEJOB_DROP, UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + + stats.put(Clutch.Metric.CPU, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.MEMORY, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.NETWORK, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.LAG, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.DROPS, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.UserDefined, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.RPS, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stats.put(Clutch.Metric.SOURCEJOB_DROP, new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + } + + public ExperimentalClutchConfigurator(IClutchMetricsRegistry metricsRegistry, Observable timer, + long initialConfigMillis, + Function1, ClutchConfiguration> configurator) { + this.metricsRegistry = metricsRegistry; + this.timer = timer; + this.initialConfigMilis = initialConfigMillis; + this.configurator = configurator; + } + + // + // Configs + // + + /** + * Generates a configuration based on Clutch's best understanding of the job at this time. + * @return A configuration suitable for autoscaling with Clutch. + */ + private ClutchConfiguration getConfig() { + return this.configurator.apply(sketches); + } + + @Override + public Observable call(Observable eventObservable) { + Observable configs = timer + .map(__ -> getConfig()) + .doOnNext(config -> log.info("New Config: {}", config.toString())); + + Observable initialConfig = Observable + .interval(this.initialConfigMilis, TimeUnit.MILLISECONDS) + .take(1) + .map(__ -> getConfig()) + .doOnNext(config -> log.info("Initial Config: {}", config.toString())); + + eventObservable + .filter(event -> event != null && event.metric != null) + .map(event -> { + UpdateDoublesSketch sketch = sketches.computeIfAbsent(event.metric, metric -> + UpdateDoublesSketch.builder().setK(DEFAULT_K).build()); + sketch.update(event.value); + + DescriptiveStatistics stat = stats.computeIfAbsent(event.metric, metric -> + new DescriptiveStatistics(NUM_STATS_DATA_POINTS)); + stat.addValue(event.value); + return null; + }).subscribe(); + + return initialConfig + .concatWith(configs) + .distinctUntilChanged() + .doOnNext(__ -> log.info("RPS Sketch State: {}", sketches.get(Clutch.Metric.RPS))) + .doOnNext(__ -> { + logSketchSummary(sketches.get(Clutch.Metric.RPS)); + logStatsSummary(stats.get(Clutch.Metric.RPS), "Stats RPS metric: "); + logStatsSummary(stats.get(Clutch.Metric.CPU), "Stats CPU metric: "); + logStatsSummary(stats.get(Clutch.Metric.MEMORY), "Stats Memory metric: "); + logStatsSummary(stats.get(Clutch.Metric.NETWORK), "Stats Network metric: "); + }) + .doOnNext(config -> log.info("Clutch switched to config: {}", config)); + } + + private static void logSketchSummary(DoublesSketch sketch) { + double[] quantiles = sketch.getQuantiles(new double[]{0.0, 0.25, 0.5, 0.75, 0.99, 1.0}); + log.info("RPS Sketch Quantiles -- Min: {}, 25th: {}, 50th: {}, 75th: {}, 99th: {}, Max: {}", + quantiles[0], + quantiles[1], + quantiles[2], + quantiles[3], + quantiles[4], + quantiles[5] + ); + } + + private static void logStatsSummary(DescriptiveStatistics stat, String prefix) { + log.info("{} RPS Sketch Quantiles -- Min: {}, 25th: {}, 50th: {}, 75th: {}, 99th: {}, Max: {}", + prefix, + stat.getPercentile(0), + stat.getPercentile(25), + stat.getPercentile(50), + stat.getPercentile(75), + stat.getPercentile(99), + stat.getPercentile(100) + ); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalControlLoop.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalControlLoop.java new file mode 100644 index 000000000..221006ca8 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/ExperimentalControlLoop.java @@ -0,0 +1,148 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import io.mantisrx.control.IActuator; +import io.mantisrx.control.controllers.ErrorComputer; +import io.mantisrx.control.controllers.Integrator; +import io.mantisrx.control.controllers.PIDController; +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import rx.Observable; +import rx.Subscription; + + +@Slf4j +public class ExperimentalControlLoop implements Observable.Transformer { + private final ClutchConfiguration config; + private final IActuator actuator; + private final AtomicDouble dampener; + + private final AtomicLong cooldownTimestamp; + private final AtomicLong currentSize; + private final AtomicDouble lastLag; + private final Observable size; + private final IRpsMetricComputer rpsMetricComputer; + private final IScaleComputer scaleComputer; + private long cooldownMillis; + + public ExperimentalControlLoop(ClutchConfiguration config, IActuator actuator, AtomicLong currentSize, + Observable timer, Observable size) { + this(config, actuator, currentSize, new AtomicDouble(1.0), timer, size, + new DefaultRpsMetricComputer(), new DefaultScaleComputer()); + } + + public ExperimentalControlLoop(ClutchConfiguration config, IActuator actuator, AtomicLong currentSize, + AtomicDouble dampener, Observable timer, Observable size, + IRpsMetricComputer rpsMetricComputer, + IScaleComputer scaleComputer) { + this.config = config; + this.actuator = actuator; + this.dampener = dampener; + this.cooldownMillis = config.getCooldownUnits().toMillis(config.cooldownInterval); + + this.cooldownTimestamp = new AtomicLong(System.currentTimeMillis()); + this.currentSize = currentSize; + this.lastLag = new AtomicDouble(0.0); + this.size = size; + this.rpsMetricComputer = rpsMetricComputer; + this.scaleComputer = scaleComputer; + } + + @Override + public Observable call(Observable events) { + events = events.share(); + + Observable lag = + Observable.just(new Event(Clutch.Metric.LAG, 0.0)) + .mergeWith(events.filter(event -> event.getMetric() == Clutch.Metric.LAG)); + + Observable drops = + Observable.just(new Event(Clutch.Metric.DROPS, 0.0)) + .mergeWith(events.filter(event -> event.getMetric() == Clutch.Metric.DROPS)); + + Observable sourceJobDrops = + Observable.just(new Event(Clutch.Metric.SOURCEJOB_DROP, 0.0)) + .mergeWith(events.filter(event -> event.getMetric() == Clutch.Metric.SOURCEJOB_DROP)); + + Observable rps = events.filter(event -> event.getMetric() == Clutch.Metric.RPS); + + Integrator deltaIntegrator = new Integrator(0, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, config.integralDecay); + + Subscription sizeSub = size + .doOnNext(currentSize::set) + .doOnNext(__ -> cooldownTimestamp.set(System.currentTimeMillis())) + .doOnNext(n -> log.info("Clutch received new scheduling update with {} workers.", n)) + .subscribe(); + + return rps + .withLatestFrom(lag, drops, sourceJobDrops, (rpsEvent, lagEvent, dropEvent, sourceDropEvent) -> { + Map metrics = new HashMap<>(); + metrics.put(rpsEvent.getMetric(), rpsEvent.getValue()); + metrics.put(lagEvent.getMetric(), lagEvent.getValue()); + metrics.put(dropEvent.getMetric(), dropEvent.getValue()); + metrics.put(sourceDropEvent.getMetric(), sourceDropEvent.getValue()); + return metrics; + }) + .doOnNext(metrics -> log.info("Latest metrics: {}", metrics)) + .map(metrics -> this.rpsMetricComputer.apply(config, metrics)) + .lift(new ErrorComputer(config.setPoint, true, config.rope._1, config.rope._2)) + .lift(new PIDController(config.kp, config.ki, config.kd, 1.0, new AtomicDouble(1.0), config.integralDecay)) + .doOnNext(d -> log.info("PID controller output: {}", d)) + .lift(deltaIntegrator) + .doOnNext(d -> log.info("Integral: {}", d)) + .filter(__ -> this.cooldownMillis == 0 || cooldownTimestamp.get() <= System.currentTimeMillis() - this.cooldownMillis) + .map(delta -> this.scaleComputer.apply(config, this.currentSize.get(), delta)) + .doOnNext(d -> log.info("New desired size: {}, existing size: {}", d, this.currentSize.get())) + .filter(scale -> this.currentSize.get() != Math.round(Math.ceil(scale))) + .lift(actuator) + .doOnNext(scale -> this.currentSize.set(Math.round(Math.ceil(scale)))) + .doOnNext(__ -> deltaIntegrator.setSum(0)) + .doOnNext(__ -> cooldownTimestamp.set(System.currentTimeMillis())) + .doOnUnsubscribe(() -> { + sizeSub.unsubscribe(); + }); + } + + /* For testing to trigger actuator on next event */ + protected void setCooldownMillis(long cooldownMillis) { + this.cooldownMillis = cooldownMillis; + } + + public static class DefaultRpsMetricComputer implements IRpsMetricComputer { + private double lastLag = 0; + + public Double apply(ClutchConfiguration config, Map metrics) { + double rps = metrics.get(Clutch.Metric.RPS); + double lag = metrics.get(Clutch.Metric.LAG); + double sourceDrops = metrics.get(Clutch.Metric.SOURCEJOB_DROP); + double drops = metrics.get(Clutch.Metric.DROPS); + double lagDerivative = lag - lastLag; + lastLag = lag; + return rps + lagDerivative + sourceDrops + drops; + } + } + + public static class DefaultScaleComputer implements IScaleComputer { + public Double apply(ClutchConfiguration config, Long currentScale, Double delta) { + return Math.min(config.maxSize, Math.max(config.minSize, currentScale + delta)); + } + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IRpsMetricComputer.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IRpsMetricComputer.java new file mode 100644 index 000000000..d04fdc69a --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IRpsMetricComputer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import io.vavr.Function2; +import java.util.Map; + +/** + * A function for computing the RPS metric to be compared against the setPoint and feed to the PID controller. + * Arguments: + * 1.) the clutch configuration for the current control loop + * 2.) a Map containing metrics for computation + * Return: + * the computed RPS metric + */ +@FunctionalInterface +public interface IRpsMetricComputer extends Function2, Double> { +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IScaleComputer.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IScaleComputer.java new file mode 100644 index 000000000..b50f8ea9c --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/IScaleComputer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import io.vavr.Function3; + +/** + * A function for computing the new scale based on the current scale and the PID controller output. + * Arguments: + * 1.) the clutch configuration for the current control loop + * 2.) the current scale + * 3.) the delta computed by the PID controller + * Return: + * the new scale, which will be acted on by the actuator + */ +@FunctionalInterface +public interface IScaleComputer extends Function3 { +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/OscillationDetector.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/OscillationDetector.java new file mode 100644 index 000000000..4d87e2ea9 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/OscillationDetector.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import io.mantisrx.control.IController; +import io.mantisrx.shaded.com.google.common.cache.Cache; +import io.mantisrx.shaded.com.google.common.cache.CacheBuilder; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * The OscillationDetctor collects scaling events from the actuator and + * determines wether the event was a scale up / down. It then computes + * a gain factor (see `OscillationDetector#computeOscillationFactor`) and sets + * the dampener based on this factor. + **/ +public class OscillationDetector extends IController { + + private final Cache history; + private Double previous = -1.0; + private final Consumer callback; + + public OscillationDetector(int historyMinutes, Consumer callback) { + this.history = CacheBuilder.newBuilder() + .maximumSize(12) + .expireAfterWrite(historyMinutes, TimeUnit.MINUTES) + .build(); + this.callback = callback; + } + + @Override + protected Double processStep(Double scale) { + + this.previous = this.previous == null ? scale : this.previous; + double delta = scale > previous ? 1.0 : -1.0; + this.previous = scale; + + history.put(System.currentTimeMillis(), delta); + this.callback.accept(computeOscillationFactor(history)); + return scale; + } + + /** + * Computes the oscillation factor which is the percentage of scaling events + * which were in different directions. + * 0.5 <= oscillationFactor <= 1.0 + * + * @param actionCache A cache of timestamp -> scale + * @return The computed oscillation factor. + */ + private double computeOscillationFactor(Cache actionCache) { + long nUp = actionCache.asMap().values().stream().filter(x -> x > 0.0).count(); + long nDown = actionCache.asMap().values().stream().filter(x -> x < 0.0).count(); + long n = nUp + nDown; + + return n == 0 + ? 1.0 + : nUp > nDown + ? (1.0 * nUp) / n + : (1.0 * nDown) / n; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/SymptomDetector.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/SymptomDetector.java new file mode 100644 index 000000000..33d5f2dbe --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/SymptomDetector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import com.yahoo.sketches.quantiles.DoublesSketch; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import java.util.List; +import java.util.function.Predicate; + +/** + * TODO: This is completely experimental, not currently in use. + */ +public class SymptomDetector { + + private final Set>>> symptoms = HashSet.empty(); + + public void registerDector(String symptom, Predicate> pred) { + symptoms.add(Tuple.of(symptom, pred)); + } + + public Set getSymptoms(List observations) { + return symptoms + .filter(tup -> tup._2.test(observations)) + .map(tup -> tup._1); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/metrics/IClutchMetricsRegistry.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/metrics/IClutchMetricsRegistry.java new file mode 100644 index 000000000..39ce7f3ea --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/clutch/metrics/IClutchMetricsRegistry.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch.metrics; + +public interface IClutchMetricsRegistry { + public default void reportOverProvisioned(String name) {} + public default void reportUnderProvisioned(String name) {} +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoostComputer.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoostComputer.java new file mode 100644 index 000000000..cb626cf53 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoostComputer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import io.mantisrx.control.IController; +import java.util.concurrent.atomic.AtomicLong; + +public class BoostComputer extends IController { + + private final AtomicLong size; + + public BoostComputer(final AtomicLong size) { + this.size = size; + } + + @Override + protected Double processStep(final Double input) { + return (input + this.size.get()) / this.size.get(); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoundToInterval.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoundToInterval.java new file mode 100644 index 000000000..ef0fdbc70 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/BoundToInterval.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import io.mantisrx.control.IController; + +public class BoundToInterval extends IController { + + private final double min; + private final double max; + + public BoundToInterval(double min, double max) { + this.min = min; + this.max = max; + } + + @Override + protected Double processStep(final Double input) { + double x = input; + return x > this.max ? this.max : + x < this.min ? this.min : + x; + } + + public static BoundToInterval of(double min, double max) { + return new BoundToInterval(min, max); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ControlLoop.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ControlLoop.java new file mode 100644 index 000000000..f6f544070 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ControlLoop.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.IActuator; +import io.mantisrx.control.clutch.Clutch; +import io.mantisrx.control.clutch.ClutchConfiguration; +import io.mantisrx.control.clutch.Event; +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import rx.Observable; + + +@Slf4j +public class ControlLoop implements Observable.Transformer { + + private final ClutchConfiguration config; + private final IActuator actuator; + private final AtomicDouble dampener; + private final AtomicLong currentScale; + private final long cooldownMillis; + private final AtomicLong cooldownTimestamp; + + private final UpdateDoublesSketch sketch = UpdateDoublesSketch.builder().setK(1024).build(); + + public ControlLoop(ClutchConfiguration config, IActuator actuator, AtomicLong initialSize) { + this(config, actuator, initialSize, new AtomicDouble(1.0)); + } + + public ControlLoop(ClutchConfiguration config, IActuator actuator, AtomicLong initialSize, + AtomicDouble dampener) { + this.config = config; + this.actuator = actuator; + this.currentScale = initialSize; + this.dampener = dampener; + this.cooldownMillis = config.getCooldownUnits().toMillis(config.cooldownInterval); + this.cooldownTimestamp = new AtomicLong(System.currentTimeMillis() + this.cooldownMillis); + } + + @Override + public Observable call(Observable events) { + events = events.share(); + + // TODO: How do I get a zero if nothing came through? + Observable lag = events.filter(event -> event.getMetric() == Clutch.Metric.LAG); + Observable drops = events.filter(event -> event.getMetric() == Clutch.Metric.DROPS); + + return events + .filter(e -> e.metric == config.metric) + .map(e -> e.value) + .doOnNext(sketch::update) + .lift(new ErrorComputer(config.setPoint, true, config.rope._1, config.rope._2)) + .lift(new PIDController(config.kp, config.ki, config.kd, 1.0, new AtomicDouble(1.0), config.integralDecay)) + .lift(new Integrator(currentScale.get(), config.minSize, config.maxSize, config.integralDecay)) + .filter(__ -> this.cooldownMillis == 0 || cooldownTimestamp.get() <= System.currentTimeMillis() - this.cooldownMillis) + .filter(scale -> this.currentScale.get() != Math.round(Math.ceil(scale))) + .lift(actuator) + .doOnNext(scale -> this.currentScale.set(Math.round(Math.ceil(scale)))) + .doOnNext(__ -> cooldownTimestamp.set(System.currentTimeMillis())); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Derivative.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Derivative.java new file mode 100644 index 000000000..17f386b58 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Derivative.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import io.mantisrx.control.IController; + +public class Derivative extends IController { + + private double last = 0; + private boolean initialized = false; + + @Override + protected Double processStep(Double input) { + + if (initialized) { + double output = input - last; + this.last = input; + + return output; + } else { + return 0.0; + } + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ErrorComputer.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ErrorComputer.java new file mode 100644 index 000000000..e9e0f97b2 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/ErrorComputer.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import io.mantisrx.control.IController; + +/** + * The loss computation is generally the first step in a control system. + * The responsibility of this component is to compute the loss function + * on the output of the system under control. + * + * Example: + * [ErrorComputer] -> PIDController -> Integrator -> Actuator + * + * The loss acts as input for the control system. + */ +public class ErrorComputer extends IController { + + private final double setPoint; + private final boolean inverted; + private final double lowerRope; + private final double upperRope; + + /** + * + * @param setPoint The target value for the metric being tracked. + * @param inverted A boolean indicating whether or not to invert output. Output is generally inverted if increasing + * the plant input will decrease the output. For example when autoscaling increasing the number + * of worker instances will decrease messages processed per instance. This is an inverted problem. + * @param lowerRope Region of practical equivalence (ROPE) -- a region surrounding the setpoint considered equal to the setpoint. + * @param upperRope Region of practical equivalence (ROPE) -- a region surrounding the setpoint considered equal to the setpoint. + */ + public ErrorComputer(double setPoint, boolean inverted, double lowerRope, double upperRope) { + this.setPoint = setPoint; + this.inverted = inverted; + this.lowerRope = lowerRope; + this.upperRope = upperRope; + } + + public ErrorComputer(double setPoint, boolean inverted, double rope) { + this.setPoint = setPoint; + this.inverted = inverted; + this.lowerRope = rope; + this.upperRope = rope; + } + + @Override + public Double processStep(Double input) { + return inverted ? + -1.0 * loss(setPoint, input, lowerRope, upperRope) : + loss(setPoint, input, lowerRope, upperRope); + } + + /** + * Computes the correct loss value considering all values within [setPoint-rope, setPoint+rope] are considered + * to be equivalent. Error must grow linearly once the value is outside of the ROPE, without a calculation such as + * this the loss is a step function once crossing the threshold, with this function loss is zero and linearly + * increases as it deviates from the setpoint and ROPE. + * + * @param setPoint The configured setPoint. + * @param observed The observed metric to be compared to the setPoint. + * @param lowerRope The region of practical equivalence (ROPE) on the lower end. + * @param upperRope The region of practical equivalence (ROPE) on the upper end. + * @return Error adjusted for the ROPE. + */ + public static double loss(double setPoint, double observed, double lowerRope, double upperRope) { + if (observed > setPoint + upperRope) { + return (setPoint + upperRope) - observed; + } else if (observed < setPoint - lowerRope) { + return (setPoint - lowerRope) - observed; + } + return 0.0; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Integrator.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Integrator.java new file mode 100644 index 000000000..573fe4f83 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/Integrator.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import io.mantisrx.control.IController; + +public class Integrator extends IController { + + private double sum = 0; + private double min = Double.NEGATIVE_INFINITY; + private double max = Double.POSITIVE_INFINITY; + private double decayFactor = 1.0; + + public Integrator() { + } + + public Integrator(double init) { + this.sum = init; + } + + public Integrator(double init, double min, double max) { + this(init, min, max, 1.0); + } + + public Integrator(double init, double min, double max, double decayFactor) { + this.sum = init; + this.min = min; + this.max = max; + this.decayFactor = decayFactor; + } + + /** + * A Clutch specific optimization, I don't like this one bit, + * and would like to clean it up before OSS probably tearing down the + * Rx pipeline and rewiring it instead. + * @param val The value to which this integrator will be set. + */ + public void setSum(double val) { + this.sum = val; + } + + @Override + protected Double processStep(Double input) { + double newSum = this.sum + input; + newSum = (newSum > max) ? max : newSum; + newSum = (newSum < min) ? min : newSum; + this.sum = decayFactor * newSum; + return newSum; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/PIDController.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/PIDController.java new file mode 100644 index 000000000..9c81ad2c9 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/controllers/PIDController.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import io.mantisrx.control.IController; +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; + +/** + * The Feedback Principle: Constantly compare the actual output to the + * setpoint; then apply a corrective action in the proper direction and + * approximately of the correct size. + * + * Iteratively applying changes in the correct direction allows this + * system to converge onto the correct value over time. + * + */ +public class PIDController extends IController { + + private final Double kp; // Proportional Gain + private final Double ki; // Integral Gain + private final Double kd; // Derivative Gain + private Double previous = 0.0; + + private final double deltaT; + private final AtomicDouble dampener; + private final double integralDecay; + + private Double integral = 0.0; + private Double derivative = 0.0; + + /** + * Implements a Proportional-Integral-Derivative (PID) three term control + * system. + * + * @param kp The gain for the proportional component of the controller. + * @param ki The gain for the integral component of the controller. + * @param kd The gain for the derivative component of the controller. + * @param deltaT The time delta. A useful default is 1.0. + * @param dampener A dampening signal which can be used for gain scheduling. + * @param integralDecay Factor [0.0, 1.0] to decay the integral component on each step. + * + * Setting the gain for an individual component disables said + * component. For example setting kd to 0.0 creates a PI (two term) control + * system. + * + * Gain scheduling is a method of manipulating the behavior of a PID + * controller at runtime. The concept is that different gain schedules might + * be appropriate at different times. Some examples; + * + * Oscillation: High gain can exacerbate and even cause oscillation. + * Chaos Kong: Increasing gain to accelerate scale ups. + */ + public PIDController(Double kp, Double ki, Double kd, Double deltaT, AtomicDouble dampener, double integralDecay) { + this.kp = kp; + this.ki = ki; + this.kd = kd; + this.deltaT = deltaT; + this.dampener = dampener; + this.integralDecay = integralDecay; + } + + public PIDController(Double kp, Double ki, Double kd) { + this(kp, ki, kd, 1.0, new AtomicDouble(1.0), 1.0); + } + + @Override + public Double processStep(Double error) { + double curIntegral = this.integral + this.deltaT * error; + this.derivative = (error - this.previous) / this.deltaT; + this.previous = error; + this.integral = this.integralDecay * curIntegral; + + double d = this.dampener.get(); + + return this.kp * d * error + + this.ki * d * curIntegral + + this.kd * d * this.derivative; + } + + /** + * @deprecated + * Use the public constructors + */ + @Deprecated + public static PIDController of(Double kp, Double ki, Double kd, AtomicDouble dampener) { + return new PIDController(kp, ki, kd, 1.0, dampener, 1.0); + } + + /** + * @deprecated + * Use the public constructors + */ + @Deprecated + public static PIDController of(Double kp, Double ki, Double kd, Double deltaT) { + return new PIDController(kp, ki, kd, deltaT, new AtomicDouble(1.0), 1.0); + } + + /** + * @deprecated + * Use the public constructors + */ + @Deprecated + public static PIDController of(Double kp, Double ki, Double kd) { + return new PIDController(kp, ki, kd, 1.0, new AtomicDouble(1.0), 1.0); + } + + public AtomicDouble getDampener() { + return this.dampener; + } + + public double getIntegralDecay() { + return this.integralDecay; + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/examples/ExampleAutoScaler.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/examples/ExampleAutoScaler.java new file mode 100644 index 000000000..782903341 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/examples/ExampleAutoScaler.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.examples; + + +import io.mantisrx.control.actuators.MantisJobActuator; +import io.mantisrx.control.controllers.ErrorComputer; +import io.mantisrx.control.controllers.Integrator; +import io.mantisrx.control.controllers.PIDController; +import rx.Observable; + +/** + * Simple example controller shows how one can create a PID which can auto-scale + * a process on a single metric. + */ +public class ExampleAutoScaler implements Observable.Transformer { + + private Double setPoint = 65.0; + private Double rope = 5.0; + + private double kp = 0.01; + private double ki = 0.01; + private double kd = 0.01; + + private double min = 1.0; + private double max = 10.0; + + @Override + public Observable call(Observable cpuMeasurements) { + + return cpuMeasurements + /* + The error computer here is going to take our stream of CPU + readings and compute an error value for our controller. + + In this case we're targeting 65.0% CPU usage, and the problem + is inverted (scaling up causes CPU to decrease). Finally we + use a region of practical equivalence (ROPE) sometimes referred + to as a dead zone around our target of 65.0. The reason for this + is that it is difficult to hit 65% CPU usage exactly. This treats + the interval [60.0, 70.0] as the setPoint. + */ + .lift(new ErrorComputer(this.setPoint, true, rope)) + /* + The controller takes the error measurements and attempts + to determine the correct scale to track the setPoint. Each of the + gain parameters can be thought of as a factor multiplied by that + particular component. + + kp: Proportional gain, multiplied by the error. + ki: Integral gain, multiplied by the sum of all errors. (Recall + error can be negative!) + kd: Derivative gain, multiplied by the diference between error now + and at t-1. + */ + .lift(new PIDController(kp, ki, kd)) + /* + The integrator's job is to sum up the output of the controller. The + reason we integrate is because our actuator expects whole size numbers. If + instead we were required to produce only the differences (+2 for scale up two + workers, -3 to scale down three then we would omit the integrator. + */ + .lift(new Integrator(1.0, min, max, 1.0)) + /* + We now feed this to an actuator which hows how to actually perform + the scaling action against the target. This can be achieved by performing a + REST call or communicating with other in-process systems. + */ + .lift(new MantisJobActuator("myJob", 1, "prod", "us-east-1", "main")) + /* + Finally we perform some logging. + */ + .doOnNext(System.out::println); + } +} diff --git a/mantis-rxcontrol/src/main/java/io/mantisrx/control/utils/OperatorToTransformer.java b/mantis-rxcontrol/src/main/java/io/mantisrx/control/utils/OperatorToTransformer.java new file mode 100644 index 000000000..5a8141027 --- /dev/null +++ b/mantis-rxcontrol/src/main/java/io/mantisrx/control/utils/OperatorToTransformer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.utils; + +import rx.Observable; + +/* + Wraps an rx Operator into an rx Transformer + */ +public class OperatorToTransformer implements Observable.Transformer { + + private final Observable.Operator op; + + public OperatorToTransformer(Observable.Operator op) { + this.op = op; + } + + @Override + public Observable call(Observable tObservable) { + return tObservable.lift(op); + } +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfigurationTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfigurationTest.java new file mode 100644 index 000000000..b375eff60 --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfigurationTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ClutchConfigurationTest { + @Test + public void shouldCreateClutchConfiguration() { + ClutchConfiguration config = ClutchConfiguration.builder().kd(1.0).build(); + assertEquals(1.0, config.kd, 1e-10); + assertEquals(1.0, config.integralDecay, 1e-10); + + config = ClutchConfiguration.builder().kd(1.0).integralDecay(0.9).build(); + assertEquals(1.0, config.kd, 1e-10); + assertEquals(0.9, config.integralDecay, 1e-10); + } +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfiguratorTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfiguratorTest.java new file mode 100644 index 000000000..7fc5befbe --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchConfiguratorTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.yahoo.sketches.quantiles.UpdateDoublesSketch; +import io.mantisrx.control.clutch.metrics.IClutchMetricsRegistry; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import org.assertj.core.data.Offset; +import org.junit.Test; +import rx.Observable; + +public class ClutchConfiguratorTest { + + + + @Test public void shouldCorrectlyBoundValues() { + assertThat(ClutchConfigurator.bound(0, 10, 50)).isEqualTo(10, Offset.offset(0.001)); + assertThat(ClutchConfigurator.bound(0, 10, -10.0)).isEqualTo(0, Offset.offset(0.001)); + assertThat(ClutchConfigurator.bound(0, 10, 5.2)).isEqualTo(5.2, Offset.offset(0.001)); + } + + @Test public void shouldProduceValeusInSaneRange() { + + ThreadLocalRandom random = ThreadLocalRandom.current(); + + UpdateDoublesSketch sketch = UpdateDoublesSketch.builder().build(1024); + for (int i = 0; i < 21; ++i) { + sketch.update(random.nextDouble(8.3, 75.0)); + } + + assertThat(sketch.getQuantile(0.99)).isLessThan(76.0); + } + + @Test public void shouldGetConfigWithoutException() { + ClutchConfigurator configurator = new ClutchConfigurator(new IClutchMetricsRegistry() {}, 1, 2, Observable.interval(1, TimeUnit.DAYS)); + configurator.getSketch(Clutch.Metric.CPU).update(70.0); + assertNotNull(configurator.getConfig()); + } + + @Test + public void testPercentileCalculation() { + + DescriptiveStatistics stats = new DescriptiveStatistics(100); + UpdateDoublesSketch sketch = UpdateDoublesSketch.builder().build(1024); + for (int i = 0; i < 200; ++i) { + sketch.update(i); + stats.addValue(i); + } + assertEquals(sketch.getQuantile(0.8), 160, 0); + assertEquals(stats.getPercentile(80), 180, 1); + } + // TODO: What guarantees do I want to make about the configurator? +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchExperimentalTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchExperimentalTest.java new file mode 100644 index 000000000..c18c71e5f --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ClutchExperimentalTest.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + + + +public class ClutchExperimentalTest { +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ControlLoopTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ControlLoopTest.java new file mode 100644 index 000000000..64b7a12b6 --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ControlLoopTest.java @@ -0,0 +1,201 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.mantisrx.control.IActuator; +import io.mantisrx.control.controllers.ControlLoop; +import io.vavr.Tuple; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Test; +import rx.Observable; +import rx.observers.TestSubscriber; + + +public class ControlLoopTest { + + @Test public void shouldRemainInSteadyState() { + ClutchConfiguration config = ClutchConfiguration.builder() + .cooldownInterval(10L) + .cooldownUnits(TimeUnit.MILLISECONDS) + .metric(Clutch.Metric.CPU) + .kd(0.01) + .kp(0.01) + .kd(0.01) + .maxSize(10) + .minSize(3) + .rope(Tuple.of(0.25, 0.0)) + .setPoint(0.6) + .build(); + + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.range(0, 1000) + .map(__ -> new Event(Clutch.Metric.CPU, 0.5)) + .compose(new ControlLoop(config, IActuator.of(x -> x), new AtomicLong(8))) + .toBlocking() + .subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + assertThat(subscriber.getOnNextEvents()).allSatisfy(x -> assertThat(x).isEqualTo(8.0)); + } + + @Test public void shouldBeUnperturbedByOtherMetrics() { + ClutchConfiguration config = ClutchConfiguration.builder() + .cooldownInterval(10L) + .cooldownUnits(TimeUnit.MILLISECONDS) + .metric(Clutch.Metric.CPU) + .kd(0.01) + .kp(0.01) + .kd(0.01) + .maxSize(10) + .minSize(3) + .rope(Tuple.of(0.25, 0.0)) + .setPoint(0.6) + .build(); + + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable cpu = Observable.range(0, 1000) + .map(__ -> new Event(Clutch.Metric.CPU, 0.5)); + + Observable network = Observable.range(0, 1000) + .map(__ -> new Event(Clutch.Metric.NETWORK, 0.1)); + + cpu.mergeWith(network) + .compose(new ControlLoop(config, IActuator.of(x -> x), new AtomicLong(8))) + .toBlocking() + .subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + assertThat(subscriber.getOnNextEvents()).allSatisfy(x -> assertThat(x).isEqualTo(8.0)); + } + + @Test public void shouldScaleUp() { + ClutchConfiguration config = ClutchConfiguration.builder() + .cooldownInterval(10L) + .cooldownUnits(TimeUnit.MILLISECONDS) + .metric(Clutch.Metric.CPU) + .kd(0.01) + .kp(0.01) + .kd(0.01) + .maxSize(10) + .minSize(3) + .rope(Tuple.of(0.25, 0.0)) + .setPoint(0.6) + .build(); + + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.range(0, 1000) + .map(__ -> new Event(Clutch.Metric.CPU, 0.7)) + .compose(new ControlLoop(config, IActuator.of(Math::ceil), new AtomicLong(8))) + .toBlocking() + .subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + assertThat(subscriber.getOnNextEvents()).allSatisfy(x -> assertThat(x).isEqualTo(9.0)); + } + + @Test public void shouldScaleDown() { + ClutchConfiguration config = ClutchConfiguration.builder() + .cooldownInterval(10L) + .cooldownUnits(TimeUnit.MILLISECONDS) + .metric(Clutch.Metric.CPU) + .kd(0.01) + .kp(0.01) + .kd(0.01) + .maxSize(10) + .minSize(3) + .rope(Tuple.of(0.25, 0.0)) + .setPoint(0.6) + .build(); + + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.range(0, 1000) + .map(__ -> new Event(Clutch.Metric.CPU, 0.2)) + .compose(new ControlLoop(config, IActuator.of(Math::ceil), new AtomicLong(8))) + .toBlocking() + .subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + assertThat(subscriber.getOnNextEvents()).allSatisfy(x -> assertThat(x).isLessThan(8.0)); + } + + @Test public void shouldScaleUpAndDown() { + ClutchConfiguration config = ClutchConfiguration.builder() + .cooldownInterval(0L) + .cooldownUnits(TimeUnit.MILLISECONDS) + .metric(Clutch.Metric.CPU) + .kd(0.1) + .kp(0.5) + .kd(0.1) + .maxSize(10) + .minSize(3) + .rope(Tuple.of(0.0, 0.0)) + .setPoint(0.6) + .build(); + + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.range(0, 1000) + .map(tick -> new Event(Clutch.Metric.CPU, ((tick % 60.0) + 30.0) / 100.0)) + .compose(new ControlLoop(config, IActuator.of(Math::ceil), new AtomicLong(5))) + .toBlocking() + .subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + assertThat(subscriber.getOnNextEvents()).anySatisfy(x -> assertThat(x).isLessThan(5.0)); + assertThat(subscriber.getOnNextEvents()).anySatisfy(x -> assertThat(x).isGreaterThan(5.0)); + } + + @Test public void shouldScaleUpAndDownWithValuesInDifferentRange() { + ClutchConfiguration config = ClutchConfiguration.builder() + .cooldownInterval(0L) + .cooldownUnits(TimeUnit.MILLISECONDS) + .metric(Clutch.Metric.CPU) + .kd(0.01) + .kp(0.05) + .kd(0.01) + .maxSize(10) + .minSize(3) + .rope(Tuple.of(0.0, 0.0)) + .setPoint(60.0) + .build(); + + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.range(0, 1000) + .map(tick -> new Event(Clutch.Metric.CPU, (tick % 60.0) + 30.0)) + .compose(new ControlLoop(config, IActuator.of(Math::ceil), new AtomicLong(5))) + .toBlocking() + .subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + assertThat(subscriber.getOnNextEvents()).anySatisfy(x -> assertThat(x).isLessThan(5.0)); + assertThat(subscriber.getOnNextEvents()).anySatisfy(x -> assertThat(x).isGreaterThan(5.0)); + } +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ExperimentalControlLoopTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ExperimentalControlLoopTest.java new file mode 100644 index 000000000..f96809533 --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/clutch/ExperimentalControlLoopTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.clutch; + +import static org.junit.Assert.assertEquals; + +import io.mantisrx.control.IActuator; +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; +import io.vavr.Tuple; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Test; +import rx.Observable; +import rx.subjects.PublishSubject; + +public class ExperimentalControlLoopTest { + + @Test + public void shouldCallActuator() throws Exception { + ClutchConfiguration config = ClutchConfiguration.builder() + .metric(Clutch.Metric.RPS) + .setPoint(100.0) + .kp(1.0) + .ki(0) + .kd(0) + .minSize(1) + .maxSize(1000) + .rope(Tuple.of(0.0, 0.0)) + .cooldownInterval(0) + .cooldownUnits(TimeUnit.SECONDS) + .build(); + TestActuator actuator = new TestActuator(); + CountDownLatch latch = actuator.createLatch(); + + ExperimentalControlLoop controlLoop = new ExperimentalControlLoop(config, actuator, new AtomicLong(100), + new AtomicDouble(1.0), Observable.timer(10, TimeUnit.MINUTES), Observable.just(100), + new ExperimentalControlLoop.DefaultRpsMetricComputer(), + new ExperimentalControlLoop.DefaultScaleComputer()); + + PublishSubject publisher = PublishSubject.create(); + controlLoop.call(publisher).subscribe(); + + publisher.onNext(new Event(Clutch.Metric.RPS, 110)); + latch.await(); + assertEquals(110, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.RPS, 120)); + latch.await(); + assertEquals(130, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.RPS, 90)); + latch.await(); + assertEquals(120, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.RPS, 0)); + latch.await(); + assertEquals(20, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.RPS, 0)); + latch.await(); + assertEquals(1, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.RPS, 2000)); + latch.await(); + assertEquals(1000, actuator.lastValue, 1e-10); + } + + @Test + public void testLagDerivativeInMetricComputer() throws Exception { + ClutchConfiguration config = ClutchConfiguration.builder() + .metric(Clutch.Metric.RPS) + .setPoint(100.0) + .kp(1.0) + .ki(0) + .kd(0) + .minSize(1) + .maxSize(1000) + .rope(Tuple.of(0.0, 0.0)) + .cooldownInterval(0) + .cooldownUnits(TimeUnit.SECONDS) + .build(); + TestActuator actuator = new TestActuator(); + CountDownLatch latch = actuator.createLatch(); + + ExperimentalControlLoop controlLoop = new ExperimentalControlLoop(config, actuator, new AtomicLong(100), + new AtomicDouble(1.0), Observable.timer(10, TimeUnit.MINUTES), Observable.just(100), + new ExperimentalControlLoop.DefaultRpsMetricComputer(), + new ExperimentalControlLoop.DefaultScaleComputer()); + + PublishSubject publisher = PublishSubject.create(); + controlLoop.call(publisher).subscribe(); + + publisher.onNext(new Event(Clutch.Metric.RPS, 110)); + latch.await(); + assertEquals(110, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.LAG, 20)); + publisher.onNext(new Event(Clutch.Metric.RPS, 110)); + latch.await(); + assertEquals(140, actuator.lastValue, 1e-10); + + latch = actuator.createLatch(); + publisher.onNext(new Event(Clutch.Metric.LAG, 10)); + publisher.onNext(new Event(Clutch.Metric.RPS, 100)); + latch.await(); + assertEquals(130, actuator.lastValue, 1e-10); + } + + @Test + public void shouldIntegrateErrorDuringCoolDown() throws Exception { + ClutchConfiguration config = ClutchConfiguration.builder() + .metric(Clutch.Metric.RPS) + .setPoint(100.0) + .kp(1.0) + .ki(0) + .kd(0) + .minSize(1) + .maxSize(1000) + .rope(Tuple.of(0.0, 0.0)) + .cooldownInterval(10) + .cooldownUnits(TimeUnit.MINUTES) + .build(); + TestActuator actuator = new TestActuator(); + CountDownLatch latch = actuator.createLatch(); + + ExperimentalControlLoop controlLoop = new ExperimentalControlLoop(config, actuator, new AtomicLong(100), + new AtomicDouble(1.0), Observable.timer(10, TimeUnit.MINUTES), Observable.just(100), + new ExperimentalControlLoop.DefaultRpsMetricComputer(), + new ExperimentalControlLoop.DefaultScaleComputer()); + + PublishSubject publisher = PublishSubject.create(); + controlLoop.call(publisher).subscribe(); + + publisher.onNext(new Event(Clutch.Metric.RPS, 110)); + assertEquals(1, latch.getCount()); + + publisher.onNext(new Event(Clutch.Metric.RPS, 120)); + assertEquals(1, latch.getCount()); + + controlLoop.setCooldownMillis(0); + publisher.onNext(new Event(Clutch.Metric.RPS, 90)); + latch.await(); + assertEquals(120, actuator.lastValue, 1e-10); + } + + @Test + public void shouldIntegrateErrorWithDecay() throws Exception { + ClutchConfiguration config = ClutchConfiguration.builder() + .metric(Clutch.Metric.RPS) + .setPoint(100.0) + .kp(1.0) + .ki(0) + .kd(0) + .integralDecay(0.9) + .minSize(1) + .maxSize(1000) + .rope(Tuple.of(0.0, 0.0)) + .cooldownInterval(10) + .cooldownUnits(TimeUnit.MINUTES) + .build(); + TestActuator actuator = new TestActuator(); + CountDownLatch latch = actuator.createLatch(); + + ExperimentalControlLoop controlLoop = new ExperimentalControlLoop(config, actuator, new AtomicLong(100), + new AtomicDouble(1.0), Observable.timer(10, TimeUnit.MINUTES), Observable.just(100), + new ExperimentalControlLoop.DefaultRpsMetricComputer(), + new ExperimentalControlLoop.DefaultScaleComputer()); + + PublishSubject publisher = PublishSubject.create(); + controlLoop.call(publisher).subscribe(); + + publisher.onNext(new Event(Clutch.Metric.RPS, 110)); + assertEquals(1, latch.getCount()); + + publisher.onNext(new Event(Clutch.Metric.RPS, 120)); + assertEquals(1, latch.getCount()); + + controlLoop.setCooldownMillis(0); + publisher.onNext(new Event(Clutch.Metric.RPS, 90)); + latch.await(); + assertEquals(116.1, actuator.lastValue, 1e-10); + } + + public static class TestActuator extends IActuator { + private double lastValue; + private CountDownLatch latch; + + public CountDownLatch createLatch() { + this.latch = new CountDownLatch(1); + return this.latch; + } + + @Override + protected Double processStep(Double value) { + this.lastValue = value; + latch.countDown(); + return value; + } + } +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/IntegratorTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/IntegratorTest.java new file mode 100644 index 000000000..9fad8c811 --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/IntegratorTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class IntegratorTest { + @Test + public void shouldIntegrateInputs() { + Integrator integrator = new Integrator(10, -100, 100, 1.0); + double output = integrator.processStep(10.0); + assertEquals(20.0, output, 1e-10); + + output = integrator.processStep(20.0); + assertEquals(40.0, output, 1e-10); + + integrator.setSum(-10.0); + output = integrator.processStep(-10.0); + assertEquals(-20.0, output, 1e-10); + } + + @Test + public void shouldSupportDecay() { + Integrator integrator = new Integrator(10, -100, 100, 0.9); + double output = integrator.processStep(10.0); + assertEquals(20.0, output, 1e-10); + + output = integrator.processStep(20.0); + assertEquals(38.0, output, 1e-10); + + output = integrator.processStep(30.0); + assertEquals(64.2, output, 1e-10); + } + + @Test + public void shouldSupportMinMax() { + Integrator integrator = new Integrator(10, -100, 100, 1.0); + double output = integrator.processStep(200.0); + assertEquals(100.0, output, 1e-10); + + output = integrator.processStep(-400.0); + assertEquals(-100.0, output, 1e-10); + } +} diff --git a/mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/PIDControllerTest.java b/mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/PIDControllerTest.java new file mode 100644 index 000000000..0c12a53a7 --- /dev/null +++ b/mantis-rxcontrol/src/test/java/io/mantisrx/control/controllers/PIDControllerTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mantisrx.control.controllers; + +import static org.junit.Assert.assertEquals; + +import io.mantisrx.shaded.com.google.common.util.concurrent.AtomicDouble; +import org.junit.Test; + +public class PIDControllerTest { + @Test + public void shouldComputeSignal() { + PIDController controller = new PIDController(1.0, 1.0, 1.0, 1.0, new AtomicDouble(1.0), 0.9); + double signal = controller.processStep(10.0); + assertEquals(30, signal, 1e-10); + + signal = controller.processStep(10.0); + // p: 10, i: 19, d: 0 + assertEquals(29, signal, 1e-10); + + signal = controller.processStep(20.0); + // p: 20, i: 27.1, d: 10 + assertEquals(67.1, signal, 1e-10); + } +} diff --git a/mantis-server/mantis-server-agent/dependencies.lock b/mantis-server/mantis-server-agent/dependencies.lock index 1cfae181b..134bdece5 100644 --- a/mantis-server/mantis-server-agent/dependencies.lock +++ b/mantis-server/mantis-server-agent/dependencies.lock @@ -109,6 +109,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -118,7 +124,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -705,6 +712,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -714,7 +727,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -844,9 +858,16 @@ ], "locked": "3.0.1" }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" + }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -871,7 +892,8 @@ }, "com.yahoo.datasketches:sketches-core": { "firstLevelTransitive": [ - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.9.1" }, @@ -947,7 +969,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" ], - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ @@ -958,7 +980,8 @@ "io.mantisrx:mantis-shaded": { "firstLevelTransitive": [ "io.mantisrx:mantis-common", - "io.mantisrx:mantis-common-serde" + "io.mantisrx:mantis-common-serde", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -995,7 +1018,8 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, @@ -1003,10 +1027,17 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.10.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "joda-time:joda-time": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1034,6 +1065,12 @@ ], "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1099,7 +1136,8 @@ "io.mantisrx:mantis-common", "io.mantisrx:mantis-remote-observable", "io.mantisrx:mantis-runtime", - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, diff --git a/mantis-source-jobs/mantis-source-job-publish/dependencies.lock b/mantis-source-jobs/mantis-source-job-publish/dependencies.lock index eab556db1..6e956453b 100644 --- a/mantis-source-jobs/mantis-source-job-publish/dependencies.lock +++ b/mantis-source-jobs/mantis-source-job-publish/dependencies.lock @@ -21,7 +21,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -110,6 +110,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -120,7 +126,8 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common", "io.mantisrx:mantis-common-serde", - "io.mantisrx:mantis-discovery-proto" + "io.mantisrx:mantis-discovery-proto", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -247,7 +254,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -336,6 +343,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -346,7 +359,8 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common", "io.mantisrx:mantis-common-serde", - "io.mantisrx:mantis-discovery-proto" + "io.mantisrx:mantis-discovery-proto", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -463,22 +477,29 @@ ], "locked": "3.0.1" }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" + }, "com.netflix.archaius:archaius2-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -488,14 +509,14 @@ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-server-worker-client" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix:mantis-rxnetty": { "firstLevelTransitive": [ @@ -511,7 +532,8 @@ }, "com.yahoo.datasketches:sketches-core": { "firstLevelTransitive": [ - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.9.1" }, @@ -605,7 +627,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" ], - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ @@ -618,7 +640,8 @@ "io.mantisrx:mantis-common", "io.mantisrx:mantis-common-serde", "io.mantisrx:mantis-connector-publish", - "io.mantisrx:mantis-discovery-proto" + "io.mantisrx:mantis-discovery-proto", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -662,7 +685,8 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, @@ -670,10 +694,17 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.9.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "joda-time:joda-time": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -692,6 +723,12 @@ ], "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -747,7 +784,8 @@ "io.mantisrx:mantis-publish-netty", "io.mantisrx:mantis-remote-observable", "io.mantisrx:mantis-runtime", - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, @@ -788,7 +826,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.spectator:spectator-api": { "firstLevelTransitive": [ @@ -877,6 +915,12 @@ ], "project": true }, + "io.mantisrx:mantis-rxcontrol": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-runtime-executor" + ], + "project": true + }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" @@ -887,7 +931,8 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-common", "io.mantisrx:mantis-common-serde", - "io.mantisrx:mantis-discovery-proto" + "io.mantisrx:mantis-discovery-proto", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -1011,22 +1056,29 @@ ], "locked": "3.0.1" }, + "com.mashape.unirest:unirest-java": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "1.4.9" + }, "com.netflix.archaius:archaius2-api": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.archaius:archaius2-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "2.7.6" + "locked": "2.7.9" }, "com.netflix.rxjava:rxjava-math": { "firstLevelTransitive": [ - "io.mantisrx:mantis-remote-observable" + "io.mantisrx:mantis-remote-observable", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.20.6" }, @@ -1036,14 +1088,14 @@ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-server-worker-client" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix.spectator:spectator-ext-ipc": { "firstLevelTransitive": [ "io.mantisrx:mantis-publish-core", "io.mantisrx:mantis-publish-netty" ], - "locked": "1.7.11" + "locked": "1.7.12" }, "com.netflix:mantis-rxnetty": { "firstLevelTransitive": [ @@ -1059,7 +1111,8 @@ }, "com.yahoo.datasketches:sketches-core": { "firstLevelTransitive": [ - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "0.9.1" }, @@ -1153,7 +1206,7 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor" ], - "locked": "1.3.20" + "project": true }, "io.mantisrx:mantis-server-worker-client": { "firstLevelTransitive": [ @@ -1166,7 +1219,8 @@ "io.mantisrx:mantis-common", "io.mantisrx:mantis-common-serde", "io.mantisrx:mantis-connector-publish", - "io.mantisrx:mantis-discovery-proto" + "io.mantisrx:mantis-discovery-proto", + "io.mantisrx:mantis-rxcontrol" ], "project": true }, @@ -1210,7 +1264,8 @@ }, "io.reactivex:rxjava": { "firstLevelTransitive": [ - "io.mantisrx:mantis-common" + "io.mantisrx:mantis-common", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.3.8" }, @@ -1218,10 +1273,17 @@ "firstLevelTransitive": [ "io.mantisrx:mantis-runtime-executor", "io.mantisrx:mantis-runtime-loader", + "io.mantisrx:mantis-rxcontrol", "io.mantisrx:mantis-shaded" ], "locked": "0.9.2" }, + "io.vavr:vavr-jackson": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "0.9.2" + }, "joda-time:joda-time": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1240,6 +1302,12 @@ ], "locked": "2017.06" }, + "org.apache.commons:commons-math3": { + "firstLevelTransitive": [ + "io.mantisrx:mantis-rxcontrol" + ], + "locked": "3.6.1" + }, "org.apache.flink:flink-core": { "firstLevelTransitive": [ "io.mantisrx:mantis-control-plane-core" @@ -1307,7 +1375,8 @@ "io.mantisrx:mantis-publish-netty", "io.mantisrx:mantis-remote-observable", "io.mantisrx:mantis-runtime", - "io.mantisrx:mantis-runtime-executor" + "io.mantisrx:mantis-runtime-executor", + "io.mantisrx:mantis-rxcontrol" ], "locked": "1.7.36" }, diff --git a/mantis-testcontainers/dependencies.lock b/mantis-testcontainers/dependencies.lock index 953ce7a8e..c12a3e31a 100644 --- a/mantis-testcontainers/dependencies.lock +++ b/mantis-testcontainers/dependencies.lock @@ -27,7 +27,7 @@ "locked": "0.4.19.1" }, "com.squareup.okhttp3:okhttp": { - "locked": "5.0.0-alpha.12" + "locked": "5.0.0-alpha.14" }, "io.mantisrx:mantis-common": { "firstLevelTransitive": [ @@ -216,7 +216,7 @@ "locked": "0.4.19.1" }, "com.squareup.okhttp3:okhttp": { - "locked": "5.0.0-alpha.12" + "locked": "5.0.0-alpha.14" }, "io.mantisrx:mantis-common": { "firstLevelTransitive": [ @@ -397,7 +397,7 @@ "locked": "0.3.1" }, "com.squareup.okhttp3:okhttp": { - "locked": "5.0.0-alpha.12" + "locked": "5.0.0-alpha.14" }, "commons-io:commons-io": { "firstLevelTransitive": [ diff --git a/settings.gradle b/settings.gradle index 37a1ab101..8b999ac74 100644 --- a/settings.gradle +++ b/settings.gradle @@ -57,6 +57,7 @@ include 'mantis-remote-observable' include 'mantis-runtime' include 'mantis-runtime-loader' include 'mantis-runtime-executor' +include 'mantis-rxcontrol' include 'mantis-source-jobs:mantis-source-job-kafka' include 'mantis-source-jobs:mantis-source-job-publish'