From 36af1a26052dc8b777df93814c0452da3be253d1 Mon Sep 17 00:00:00 2001 From: Geir Horn Date: Fri, 26 Jan 2024 19:14:15 +0100 Subject: [PATCH] Metric updater subscribes to SLO Violation detection messages and the default objective function is stated in the now structured AMPL file. Change-Id: I8b1a7e2b5fde680f353d8cc8d8219f3e2d3e6691 --- .vscode/c_cpp_properties.json | 6 +-- AMPLSolver.cpp | 97 +++++++++++++++++++++++++++++------ AMPLSolver.hpp | 21 +++++++- MetricUpdater.cpp | 11 ++-- MetricUpdater.hpp | 20 ++------ Solver.hpp | 65 ++++++++++++++++++----- 6 files changed, 167 insertions(+), 53 deletions(-) diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 4395a9e..52452a6 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -3,8 +3,8 @@ { "name": "Linux", "includePath": [ + "/home/GHo/Documents/Code/Theron++/", "/home/GHo/Documents/Code/CxxOpts/include", - "/home/GHo/Documents/Code/Theron++", "/opt/AMPL/amplapi/include/", "${workspaceFolder}/**", "/usr/lib/gcc/x86_64-redhat-linux/13/../../../../include/c++/13" @@ -15,7 +15,7 @@ "compilerPath": "/usr/bin/g++", "compilerArgs": [ "-std=c++23", - "-I/home/GHo/Documents/Code/Theron++", + "-I/home/GHo/Documents/Code/Theron++/", "-I/home/GHo/Documents/Code/CxxOpts/include", "-I/opt/AMPL/amplapi/include/" ], @@ -24,7 +24,7 @@ "configurationProvider": "ms-vscode.makefile-tools", "browse": { "path": [ - "/home/GHo/Documents/Code/Theron++", + "/home/GHo/Documents/Code/Theron++/", "/home/GHo/Documents/Code/CxxOpts/include", "/opt/AMPL/amplapi/include/", "/usr/lib/gcc/x86_64-redhat-linux/13/../../../../include/c++/13" diff --git a/AMPLSolver.cpp b/AMPLSolver.cpp index cd8a42f..c03c3c1 100644 --- a/AMPLSolver.cpp +++ b/AMPLSolver.cpp @@ -32,23 +32,20 @@ std::string AMPLSolver::SaveFile( const JSON & TheMessage, { if( TheMessage.is_object() ) { - // Writing the problem file based on the message content that should be - // only a single key-value pair. If the file could not be opened, a run - // time exception is thrown. - std::string TheFileName - = ProblemFileDirectory / TheMessage.begin().key(); + = ProblemFileDirectory / TheMessage.at( AMPLSolver::FileName ); - std::fstream ProblemFile( TheFileName, std::ios::out ); + std::fstream ProblemFile( TheFileName, std::ios::out | std::ios::binary ); if( ProblemFile.is_open() ) { - ProblemFile << TheMessage.begin().value(); + ProblemFile << TheMessage.at( AMPLSolver::FileContent ); ProblemFile.close(); return TheFileName; } else { + std::source_location Location = std::source_location::current(); std::ostringstream ErrorMessage; ErrorMessage << "[" << Location.file_name() << " at line " @@ -64,6 +61,7 @@ std::string AMPLSolver::SaveFile( const JSON & TheMessage, } else { + std::source_location Location = std::source_location::current(); std::ostringstream ErrorMessage; ErrorMessage << "[" << Location.file_name() << " at line " @@ -95,10 +93,30 @@ void AMPLSolver::DefineProblem(const Solver::OptimisationProblem & TheProblem, const Address TheOracle) { Theron::ConsoleOutput Output; - Output << "AMPL Solver received the AMPL problem: " << TheProblem.dump(2) + Output << "AMPL Solver received the AMPL problem: " << std::endl + << TheProblem.dump(2) << std::endl; //ProblemDefinition.read( SaveFile( TheProblem ) ); + + if( TheProblem.contains( Solver::ObjectiveFunctionLabel ) ) + DefaultObjectiveFunction = TheProblem.at( Solver::ObjectiveFunctionLabel ); + else + { + std::source_location Location = std::source_location::current(); + std::ostringstream ErrorMessage; + + ErrorMessage << "[" << Location.file_name() << " at line " + << Location.line() + << "in function " << Location.function_name() <<"] " + << "The problem definition must contain a default objective " + << "function under the key [" + << Solver::ObjectiveFunctionLabel + << "]" << std::endl; + + throw std::invalid_argument( ErrorMessage.str() ); + } + Output << "Problem loaded!" << std::endl; } @@ -168,16 +186,62 @@ void AMPLSolver::SolveProblem( // objective functions as 'dropped'. Note that this is experimental code // as the multi-objective possibilities in AMPL are not well documented. - std::string - OptimisationGoal = TheContext.at( Solver::ObjectiveFunctionLabel ); + std::string OptimisationGoal; + + if( TheContext.contains( Solver::ObjectiveFunctionLabel ) ) + OptimisationGoal = TheContext.at( Solver::ObjectiveFunctionLabel ); + else if( !DefaultObjectiveFunction.empty() ) + OptimisationGoal = DefaultObjectiveFunction; + else + { + std::source_location Location = std::source_location::current(); + std::ostringstream ErrorMessage; + + ErrorMessage << "[" << Location.file_name() << " at line " + << Location.line() + << "in function " << Location.function_name() <<"] " + << "No default objective function is defined and " + << "the Application Execution Context message did " + << "not define an objective function:" + << std::endl << TheContext.dump(2) + << std::endl; + + throw std::invalid_argument( ErrorMessage.str() ); + } + + // The objective function name given must correspond to a function + // defined in the model, which implies that one function must be + // activated. + + bool ObjectiveFunctionActivated = false; for( auto TheObjective : ProblemDefinition.getObjectives() ) if( TheObjective.name() == OptimisationGoal ) + { TheObjective.restore(); + ObjectiveFunctionActivated = true; + } else TheObjective.drop(); - - // The problem can then be solved. + + // An exception is thrown if there is no objective function activated + + if( !ObjectiveFunctionActivated ) + { + std::source_location Location = std::source_location::current(); + std::ostringstream ErrorMessage; + + ErrorMessage << "[" << Location.file_name() << " at line " + << Location.line() + << "in function " << Location.function_name() <<"] " + << "The objective function label " << OptimisationGoal + << " does not correspond to any objective function in the " + << "model" << std::endl; + + throw std::invalid_argument( ErrorMessage.str() ); + } + + // The problem is valid and can then be solved. Optimize(); @@ -199,10 +263,10 @@ void AMPLSolver::SolveProblem( // The found solution can then be returned to the requesting actor or topic Send( Solver::Solution( - TheContext.at( Solver::ContextIdentifier ), TheContext.at( Solver::TimeStamp ).get< Solver::TimePointType >(), - TheContext.at( Solver::ObjectiveFunctionLabel ), - ObjectiveValues, VariableValues + TheContext.at( Solver::ObjectiveFunctionLabel ), //TO DO: Where does this come from? + ObjectiveValues, VariableValues, + TheContext.at( DeploymentFlag ).get() ), TheRequester ); } @@ -229,7 +293,8 @@ AMPLSolver::AMPLSolver( const std::string & TheActorName, NetworkingActor( Actor::GetAddress().AsString() ), Solver( Actor::GetAddress().AsString() ), ProblemFileDirectory( ProblemPath ), - ProblemDefinition( InstallationDirectory ) + ProblemDefinition( InstallationDirectory ), + DefaultObjectiveFunction() { RegisterHandler( this, &LSolver::DataFileUpdate ); diff --git a/AMPLSolver.hpp b/AMPLSolver.hpp index 80e6094..2851e87 100644 --- a/AMPLSolver.hpp +++ b/AMPLSolver.hpp @@ -127,7 +127,26 @@ protected: // constant string static constexpr std::string_view AMPLProblemTopic - = "AMPL::OptimisationProblem"; + = "eu.nebulouscloud.optimiser.solver.model"; + + // The JSON message received on this topic is supposed to contain three keys + // 1) The filename of the problem file + // 2) The file content as a single string + // 3) The default objective function (defined in the Solver class) + + static constexpr std::string_view FileName = "FileName", + FileContent = "FileContent"; + + // The AMPL problem file can contain many objective functions, but can be + // solved only for one objective function at the time. The name of the + // default objective function is therefore stored together with the model + // in the above Define Problem handler. If the default objective function + // label is not provided with the optimisation problem message, an + // invalid argument exception will be thrown. + +private: + + std::string DefaultObjectiveFunction; // -------------------------------------------------------------------------- // Data file updates diff --git a/MetricUpdater.cpp b/MetricUpdater.cpp index 39492b9..a817d17 100644 --- a/MetricUpdater.cpp +++ b/MetricUpdater.cpp @@ -156,14 +156,15 @@ void MetricUpdater::SLOViolationHandler( // message provided that the size of the execution context equals the // number of metric values. It will be different if any of the metric // values has not been updated, and in this case the application execution - // context is invalid and cannot be used for optimisation. + // context is invalid and cannot be used for optimisation and the + // SLO violation event will just be ignored. Finally, the flag indicating + // that the corresponding solution found for this application execution + // context should actually be enacted and deployed. if( TheApplicationExecutionContext.size() == MetricValues.size() ) Send( Solver::ApplicationExecutionContext( - SeverityMessage[ NebulOuS::SLOIdentifier ], - SeverityMessage[ NebulOuS::TimePoint ].get< Solver::TimePointType >(), - SeverityMessage[ NebulOuS::ObjectiveFunctionName ], - TheApplicationExecutionContext + SeverityMessage.at( NebulOuS::TimePoint ).get< Solver::TimePointType >(), + TheApplicationExecutionContext, true ), TheSolverManager ); } diff --git a/MetricUpdater.hpp b/MetricUpdater.hpp index 708f123..ecfde8f 100644 --- a/MetricUpdater.hpp +++ b/MetricUpdater.hpp @@ -123,19 +123,11 @@ constexpr std::string_view MetricValueRootString // compared to a threshold, currently set to zero to ensure that every event // message will trigger a reconfiguration. // -// However, the Metric updater will get this message from the Optimiser -// Controller component only if an update must be made. The message must -// contain a unique identifier, a time point for the solution, and the objective -// function to be maximised. - -constexpr std::string_view SLOIdentifier = "Identifier"; -constexpr std::string_view ObjectiveFunctionName = "ObjectiveFunction"; - // The messages from the Optimizer Controller will be sent on a topic that // should follow some standard topic convention. constexpr std::string_view SLOViolationTopic - = "eu.nebulouscloud.optimiser.solver.slo"; + = "eu.nebulouscloud.monitoring.slo.severity_value"; /*============================================================================== @@ -265,12 +257,10 @@ private: // The SLO Violation detector publishes an event to indicate that at least // one of the constraints for the application deployment will be violated in // the predicted future, and that the search for a new solution should start. - // This message is caught by the Optimisation Controller and republished - // adding a unique event identifier enabling the Optimisation Controller to - // match the produced solution with the event and deploy the right - // configuration.The message must also contain the name of the objective - // function to maximise. This name must match the name in the optimisation - // model sent to the solver. + // This will trigger the the publication of the Solver's Application Execution + // context message. The context message will contain the current status of the + // metric values, and trigger a solver to find a new, optimal variable + // assignment to be deployed to resolve the identified problem. class SLOViolation : public Theron::AMQ::JSONTopicMessage diff --git a/Solver.hpp b/Solver.hpp index 87c8a9c..3ed70b9 100644 --- a/Solver.hpp +++ b/Solver.hpp @@ -101,6 +101,12 @@ public: // though all objective function values will be returned with the solution, // the solution will maximise only the objective function whose label is // given in the application execution context request message. + // + // The Application Execution Cntext message may contain the name of the + // objective function to maximise. If so, this should be stored under the + // key name indicated here. However, if the objective function name is not + // given, the default objective function is used. The default objective + // function will be named when defining the optimisation problem. static constexpr std::string_view ObjectiveFunctionLabel = "ObjectiveFunction"; @@ -111,6 +117,19 @@ public: static constexpr std::string_view ExecutionContext = "ExecutionContext"; + // Finally, the execution context can come from the Metric Collector actor + // as a consequence of an SLO Violation being detected. In this case the + // optimised solution found by the solver should trigger a reconfiguration. + // However, various application execution context can also be tried for + // simulating future events and to investigate which configuration would be + // the best for these situations. In this case the optimised solution should + // not reconfigure the running application. For this reason there is a flag + // in the message indicating whether the solution should be deployed, and + // its default value is 'false' to prevent solutions form accidentially being + // deployed. + + static constexpr std::string_view DeploymentFlag = "DeploySolution"; + // To ensure that the execution context is correctly provided by the senders // The expected metric value structure is defined as a type based on the // standard unsorted map based on a JSON value object since this can hold @@ -144,25 +163,44 @@ public: static constexpr std::string_view MessageIdentifier = "eu.nebulouscloud.optimiser.solver.context"; - ApplicationExecutionContext( const ContextIdentifierType & TheIdentifier, - const TimePointType MicroSecondTimePoint, + ApplicationExecutionContext( const TimePointType MicroSecondTimePoint, const std::string ObjectiveFunctionID, - const MetricValueType & TheContext ) + const MetricValueType & TheContext, + bool DeploySolution = false ) : JSONTopicMessage( std::string( MessageIdentifier ), - { { std::string( ContextIdentifier ), TheIdentifier }, - { std::string( TimeStamp ), MicroSecondTimePoint }, + { { std::string( TimeStamp ), MicroSecondTimePoint }, { std::string( ObjectiveFunctionLabel ), ObjectiveFunctionID }, - { std::string( ExecutionContext ), TheContext } } - ) {} + { std::string( ExecutionContext ), TheContext }, + { std::string( DeploymentFlag ), DeploySolution } + }) {} + + // The constructor omitting the objective function identifier is similar + // but without the objective function string. + + ApplicationExecutionContext( const TimePointType MicroSecondTimePoint, + const MetricValueType & TheContext, + bool DeploySolution = false ) + : JSONTopicMessage( std::string( MessageIdentifier ), + { { std::string( TimeStamp ), MicroSecondTimePoint }, + { std::string( ExecutionContext ), TheContext }, + { std::string( DeploymentFlag ), DeploySolution } + }) {} + + // The copy constructor simply passes the job on to the JSON Topic + // message for copying the message ApplicationExecutionContext( const ApplicationExecutionContext & Other ) : JSONTopicMessage( Other ) {} + // The default constructor simply stores the message identifier + ApplicationExecutionContext() : JSONTopicMessage( std::string( MessageIdentifier ) ) {} + // The default destrucor is used + virtual ~ApplicationExecutionContext() = default; }; @@ -210,17 +248,18 @@ public: static constexpr std::string_view MessageIdentifier = "eu.nebulouscloud.optimiser.solver.solution"; - Solution( const ContextIdentifierType & TheIdentifier, - const TimePointType MicroSecondTimePoint, + Solution( const TimePointType MicroSecondTimePoint, const std::string ObjectiveFunctionID, const ObjectiveValuesType & TheObjectiveValues, - const VariableValuesType & TheVariables ) + const VariableValuesType & TheVariables, + bool DeploySolution ) : JSONTopicMessage( std::string( MessageIdentifier ) , - { { std::string( ContextIdentifier ), TheIdentifier }, - { std::string( TimeStamp ), MicroSecondTimePoint }, + { { std::string( TimeStamp ), MicroSecondTimePoint }, { std::string( ObjectiveFunctionLabel ), ObjectiveFunctionID }, { std::string( ObjectiveValues ) , TheObjectiveValues }, - { std::string( VariableValues ), TheVariables } } ) + { std::string( VariableValues ), TheVariables }, + { std::string( DeploymentFlag ), DeploySolution } + } ) {} Solution()