optimiser-solver/SolverComponent.cpp
Geir Horn 5c67c7c7e0 Final first release now with application ID filtering effective
Change-Id: Ief16b069c20394f3f48dcd038b57766b6c3771c0
2024-02-14 22:00:36 +01:00

357 lines
14 KiB
C++

/*==============================================================================
Solver Component
This is the main file for the Solver Component executable including the parsing
of command line arguments and the AMQ network interface. It first starts the
AMQ interface actors of the Network Endpoint, then creates the actors of the
solver component: The Metric Updater and the Solution Manager, which in turn
will start the solver actor(s). All actors are executing on proper operating
system threads, and they are scheduled for execution whenever they have a
pending message.
The command line arguments that can be givne to the Solver Component are
-A or --AMPLDir <installation directory> for the AMPL model interpreter
-B or --broker <URL> for the location of the AMQ broker
-E or --endpoint <name> The endpoint name = application identifier
-M ir --ModelDir <directory> for model and data files
-N or --name The AMQ identity of the solver (see below)
-P or --port <n> the port to use on the AMQ broker URL
-S or --Solver <label> The back-end solver used by AMPL
-U or --user <user> the user to authenticate for the AMQ broker
-Pw or --password <password> the AMQ broker password for the user
-? or --Help prints a help message for the options
Default values:
-A taken from the standard AMPL environment variables if omitted
-B localhost
-E <no default - must be given>
-M <temporary directory created by the OS>
-N "NebulOuS::Solver"
-P 5672
-S couenne
-U admin
-Pw admin
A note on the mandatory endpoint name defining the extension used for the
solver component when connecting to the AMQ server. Typically the connection
will be established as "name@endpoint" and so if there are several
solver components running, the endpoint is the only way for the AMQ solvers to
distinguish the different solver component subscriptions.
Notes on use:
The path to the AMPL API shared libray must be in the LIB path environment
variable. For instance, the installation of AMPL on the author's machine is in
/opt/AMPL and so the first thing to ensure is that the path to the API library
directory is added to the link library path, e.g.,
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/AMPL/amplapi/lib
The AMPL directory also needs to be in the path variable, and the path must
be extended with the AMPL execution file path, e.g.,
export PATH=$PATH:/opt/AMPL
The parameters to the application are used as described above, and typically the
endpoint is set to some unique identifier of the application for which this
solver is used, e.g.,
./SolverComponent --AMPLDir /opt/AMPL \
--ModelDir AMPLTest/ --Endpoint f81ee-b42a8-a13d56-e28ec9-2f5578
Debugging after a coredump
coredumpctl debug SolverComponent
Author and Copyright: Geir Horn, University of Oslo
Contact: Geir.Horn@mn.uio.no
License: MPL2.0 (https://www.mozilla.org/en-US/MPL/2.0/)
==============================================================================*/
// Standard headers
#include <string> // For standard strings
#include <source_location> // Making informative error messages
#include <sstream> // To format error messages
#include <stdexcept> // standard exceptions
#include <filesystem> // Access to the file system
#include <map> // For extended AMQ properties
// Theron++ headers
#include "Actor.hpp"
#include "Utility/StandardFallbackHandler.hpp"
#include "Utility/ConsolePrint.hpp"
#include "Communication/PolymorphicMessage.hpp"
#include "Communication/NetworkingActor.hpp"
// AMQ protocol related headers
#include "proton/symbol.hpp" // AMQ symbols
#include "proton/connection_options.hpp" // Options for the Broker
#include "proton/message.hpp" // AMQ messages definitions
#include "proton/source_options.hpp" // App ID filters
#include "proton/source.hpp" // The filter map
#include "Communication/AMQ/AMQMessage.hpp" // The AMQP messages
#include "Communication/AMQ/AMQEndpoint.hpp" // The AMP endpoint
#include "Communication/AMQ/AMQjson.hpp" // Transparent JSON-AMQP
// The cxxopts command line options parser that can be cloned from
// https://github.com/jarro2783/cxxopts
#include "cxxopts.hpp"
// AMPL Application Programmer Interface (API)
#include "ampl/ampl.h"
// NegulOuS related headers
#include "MetricUpdater.hpp"
#include "SolverManager.hpp"
#include "AMPLSolver.hpp"
/*==============================================================================
Main
==============================================================================*/
int main( int NumberOfCLIOptions, char ** CLIOptionStrings )
{
// --------------------------------------------------------------------------
// Defining and parsing the Command Line Interface (CLI) options
// --------------------------------------------------------------------------
cxxopts::Options CLIOptions("./SolverComponent",
"The NebulOuS Solver component");
CLIOptions.add_options()
("A,AMPLDir", "The AMPL installation path",
cxxopts::value<std::string>()->default_value("") )
("B,Broker", "The URL of the AMQ broker",
cxxopts::value<std::string>()->default_value("localhost") )
("E,Endpoint", "The endpoint name", cxxopts::value<std::string>() )
("M,ModelDir", "Directory to store the model and its data",
cxxopts::value<std::string>()->default_value("") )
("N,Name", "The name of the Solver Component",
cxxopts::value<std::string>()->default_value("NebulOuS::Solver") )
("P,Port", "TCP port on AMQ Broker",
cxxopts::value<unsigned int>()->default_value("5672") )
("S,Solver", "Solver to use, devault Couenne",
cxxopts::value<std::string>()->default_value("couenne") )
("U,User", "The user name used for the AMQ Broker connection",
cxxopts::value<std::string>()->default_value("admin") )
("Pw,Password", "The password for the AMQ Broker connection",
cxxopts::value<std::string>()->default_value("admin") )
("h,help", "Print help information");
CLIOptions.allow_unrecognised_options();
auto CLIValues = CLIOptions.parse( NumberOfCLIOptions, CLIOptionStrings );
if( CLIValues.count("help") )
{
std::cout << CLIOptions.help() << std::endl;
exit( EXIT_SUCCESS );
}
// --------------------------------------------------------------------------
// Validating directories
// --------------------------------------------------------------------------
//
// The directories are given as strings and they must be validated to see if
// the provided values correspond to an existing directory in the case of the
// AMPL directory. The model directory will be created if it is not an empty
// string, for which a temparary directory will be created.
std::filesystem::path TheAMPLDirectory( CLIValues["AMPLDir"].as<std::string>() );
if( !std::filesystem::exists( TheAMPLDirectory ) )
{
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 AMPL installation driectory is given as ["
<< CLIValues["AMPLDir"].as<std::string>()
<< "] but this directory does not ezist!";
throw std::invalid_argument( ErrorMessage.str() );
}
std::filesystem::path ModelDirectory( CLIValues["ModelDir"].as<std::string>() );
if( ModelDirectory.empty() || !std::filesystem::exists( ModelDirectory ) )
ModelDirectory = std::filesystem::temp_directory_path();
// --------------------------------------------------------------------------
// AMQ options
// --------------------------------------------------------------------------
//
// In order to be general and flexible, the various AMQ options must be
// provided as a user specified class to allow the user full fexibility in
// deciding on the connection properties. This class should keep the user
// name, the password, and the application identifier, which is identical
// to the endpoint.
class AMQOptions
: public Theron::AMQ::NetworkLayer::AMQProperties
{
private:
const std::string User, Password, ApplicationID;
protected:
// The connection options just sets the user and the password to be used
// when the first connection is established with the AMQ broker
virtual proton::connection_options ConnectionOptions(void) const override
{
proton::connection_options Options(
Theron::AMQ::NetworkLayer::AMQProperties::ConnectionOptions() );
Options.user( User );
Options.password( Password );
return Options;
};
// Setting the application filter is slightly more complicated as it
// involves setting the filter map for the sender. However, this is not
// well documented and the current implmenentation is based on the
// example for an earlier Proton version (0.32.0) and the example at
// https://qpid.apache.org/releases/qpid-proton-0.32.0/proton/cpp/examples/selected_recv.cpp.html
virtual proton::receiver_options ReceiverOptions( void ) const override
{
proton::source::filter_map TheFilter;
proton::source_options TheSourceOptions;
proton::symbol FilterKey("selector");
proton::value FilterValue;
proton::codec::encoder EncodedFilter( FilterValue );
proton::receiver_options TheOptions(
Theron::AMQ::NetworkLayer::AMQProperties::ReceiverOptions() );
std::ostringstream SelectorString;
SelectorString << "application = '" << ApplicationID << "'";
EncodedFilter << proton::codec::start::described()
<< proton::symbol("apache.org:selector-filter:string")
<< SelectorString.str()
<< proton::codec::finish();
TheFilter.put( FilterKey, FilterValue );
TheSourceOptions.filters( TheFilter );
TheOptions.source( TheSourceOptions );
return TheOptions;
}
// The application identifier must also be provided in every message to
// allow other receivers to filter on this.
virtual proton::message::property_map
MessageProperties( void ) const override
{
proton::message::property_map TheProperties(
Theron::AMQ::NetworkLayer::AMQProperties::MessageProperties() );
TheProperties.put( "application", ApplicationID );
return TheProperties;
}
public:
AMQOptions( const std::string & TheUser, const std::string & ThePassword,
const std::string & TheAppID )
: User( TheUser ), Password( ThePassword ), ApplicationID( TheAppID )
{}
AMQOptions( const AMQOptions & Other )
: User( Other.User ), Password( Other.Password ),
ApplicationID( Other.ApplicationID )
{}
virtual ~AMQOptions() = default;
};
// --------------------------------------------------------------------------
// AMQ communication
// --------------------------------------------------------------------------
//
// The AMQ communication is managed by the standard communication actors of
// the Theron++ Actor framewokr. Thus, it is just a matter of starting the
// endpoint actors with the given command line parameters.
//
// The network endpoint takes the endpoint name as the first argument, then
// the URL for the broker and the port number. Then the network endpoint can
// be constructed using the default names for the Session Layer and the
// Presentation layer servers, but calling the endpoint for "Solver" to make
// it more visible at the AMQ broker listing of subscribers. The endpoint
// will be a unique application identifier. The server names are followed
// by the defined AMQ options.
Theron::AMQ::NetworkEndpoint AMQNetWork(
CLIValues["Endpoint"].as< std::string >(),
CLIValues["Broker"].as< std::string >(),
CLIValues["Port"].as< unsigned int >(),
CLIValues["Name"].as< std::string >(),
Theron::AMQ::Network::SessionLayerLabel,
Theron::AMQ::Network::PresentationLayerLabel,
std::make_shared< AMQOptions >(
CLIValues["User"].as< std::string >(),
CLIValues["Password"].as< std::string >(),
CLIValues["Endpoint"].as< std::string >()
)
);
// --------------------------------------------------------------------------
// Solver component actors
// --------------------------------------------------------------------------
//
// The solver managager must be started first since its address should be
// a parameter to the constructor of the Metric Updater so the latter actor
// knows where to send application execution contexts whenever a new solution
// is requested by the SLO Violation Detector through the Optimzer Controller.
// Then follows the number of solvers to use in the solver pool and the root
// name of the solvers. This root name string will be extended with _n where n
// where n is a sequence number from 1.As all solvers are of the same type
// given by the template parameter (here AMPLSolver), they are assumed to need
// the same set of constructor arguments and the constructor arguments follow
// the root solver name.
NebulOuS::SolverManager< NebulOuS::AMPLSolver >
WorkloadMabager( CLIValues["Name"].as<std::string>(),
std::string( NebulOuS::Solver::Solution::MessageIdentifier ),
std::string( NebulOuS::Solver::ApplicationExecutionContext::MessageIdentifier ),
1, "AMPLSolver",
ampl::Environment( TheAMPLDirectory.native() ), ModelDirectory,
CLIValues["Solver"].as<std::string>() );
NebulOuS::MetricUpdater
ContextMabager( "MetricUpdater", WorkloadMabager.GetAddress() );
// --------------------------------------------------------------------------
// Termination management
// --------------------------------------------------------------------------
//
// The critical part is to wait for the global shut down message from the
// Optimiser controller. That message will trigger the network to shut down
// and the Solver Component may terminate when the actor system has finished.
// Thus, the actors can still be running for some time after the global shut
// down message has been received, and it is therefore necessary to also wait
// for the actors to terminate.
NebulOuS::ExecutionControl::WaitForTermination();
Theron::Actor::WaitForGlobalTermination();
return EXIT_SUCCESS;
}