/*******************************************************
Xpress Executor Example Model
=============================
file coco-mps-xe.js
```````````````````
Demonstrates executing the 'coco.mps' model using Xpress Executor
and displays the results. This example is written in JavaScript and
intended to be run using node.js.
Local prerequisites:
node.js
npm
Instructions
1) Configure your Executor component to solve MPS/LP files
2) Fill in the DMP_* variables with the details of your Executor component
3) Open a command prompt and type: npm install
4) When that completes, type: node coco-mps-xe.js
(c) 2017 Fair Isaac Corporation
author: J. Farmer, May. 2017
*******************************************************/
// The REST endpoint of the Xpress Executor DMP component
// ! Obtain this by clicking "View Links" for the Xpress Executor component on the DMP UI
var DMP_XE_REST_ENDPOINT="https://e2hbu1a7ma-e2hbu1a7ma.us-west.dmsuitecloud.com/rest/runtime/execution?solutionID=e2gzewa9sc";
// The client ID of solution containing the Xpress Executor DMP component
// Obtain this through the DMP UI
var DMP_SOLUTION_CLIENT_ID="e2gzewa9sc";
// The secret of the solution containing the Xpress Executor DMP component
// Obtain this through the DMP UI
var DMP_SOLUTION_SECRET="4U3uD128Jj8x4cQm*zCjPZQwdqWPj9OEk57V";
// The root DMP manager URL. This will be different depending on which instance of DMP you are using.
var DMP_MANAGER_URL="https://manager-svc.us-west.dmsuitecloud.com";
// The input file for the remote model
var MODELFILE="../model/coco.mps.gz";
// The file to which to write the solution
var SOLUTIONFILE="solution.slx";
// Any additional parameters to set
var MODELPARAMS={
PROBLEM_FORMAT: "MPS.GZ", // MPS file is gzipped
SOLUTION_FORMAT: "SLX.GZ", // Download solution file in gzipped format
XPRS_MAXTIME: -1800 // Expect solver to take no longer than 30 minutes
}
// Third-party dependency: request
// for aysynchronous HTTP requests
var request = require('request');
// Third-party dependency: prequest
// for promise-based HTTP requests
var prequest = require('prequest');
// Third-party dependency: delay
// for promise-based delays
var delay = require('delay');
// Third-party dependency: CombinedStream
// for concatenating multiple streams
var CombinedStream = require('combined-stream2');
// Third-party dependency: StringToStream
// for creating stream from string
var StringToStream = require('string-to-stream');
// Standard node.js filesystem module
var fs = require('fs');
// Standard node.js URL handling module
var url = require('url');
// Standard node.js compression handling module
var zlib = require('zlib');
// Standard node.js stream module
var stream = require('stream');
// Map from status codes to meaningful names returned by Xpress Executor
var solverStatusCodes = {
0: "NOT_STARTED",
1: "LOCALLY OPTIMAL",
2: "OPTIMAL",
3: "LOCALLY INFEASIBLE",
4: "INFEASIBLE",
5: "UNBOUNDED",
6: "UNFINISHED"
};
// Create a basic 'WallTimer' class - this measures time elapsed, in seconds, between calls to start() and stop()
function WallTimer() {
this.startTime = null;
this.endTime = null;
this.elapsedSeconds = null;
}
WallTimer.prototype.start = function() {
if (this.startTime!=null) throw new Error("timer already started");
this.startTime = process.hrtime();
};
WallTimer.prototype.stop = function() {
if (this.startTime==null) throw new Error("timer not started");
if (this.endTime!=null) throw new Error("timer already stopped");
this.endTime = process.hrtime();
this.elapsedSeconds = (this.endTime[0]-this.startTime[0])+
( (Math.round(this.endTime[1]/1e6)/1e3) - (Math.round(this.endTime[1]/1e6)/1e3) );
}
// Request an authentication token from DMP
console.log("Requesting authorization token from DMP");
var authorizationToken;
var timers = {};
timers.authRequest = new WallTimer();
timers.authRequest.start();
prequest({
method: 'POST',
url: DMP_MANAGER_URL+"/registration/rest/client/token",
body: {
clientId: DMP_SOLUTION_CLIENT_ID,
secret: DMP_SOLUTION_SECRET
}
}).then(function(body) {
// This token can be re-used in subsequent requests, but should be refreshed every half hour
authorizationToken = body;
timers.authRequest.stop();
console.log("Obtained authorization token after "+timers.authRequest.elapsedSeconds+"s");
// Start the execution of the model in our Xpress Executor service
console.log("Requesting execution");
timers.execRequest = new WallTimer();
timers.execRequest.start();
// Note: because the input body might be large, we stream the request rather than using prequest, and also use
// the multipart-form upload API rather than the REST API for this request. If we know
// the MPS file is small enough to be base64-encoded into a JavaScript string, the below could be replaced with:
// return prequest({
// method: 'POST',
// url: DMP_XE_REST_ENDPOINT,
// headers: {
// "Authorization": 'Bearer '+authorizationToken
// },
// body: {
// parameters: MODELPARAMS,
// inputBase64: new Buffer(fs.readFileSync(MODELFILE)).toString('base64')
// }
// });
return new Promise(function(resolvePromise,rejectPromise) {
var boundary = "----OptimizationExecutorNodeJSFormMessageBoundary";
requestOptions = {
method: 'POST',
url: DMP_XE_REST_ENDPOINT,
headers: {
"Authorization": 'Bearer '+authorizationToken,
"Content-Type": "multipart/form-data; boundary="+boundary,
"Accepts": "text/html"
}
};
// Pipe from execRequestStream to the request
var execRequestStream = CombinedStream.create();
// Construct the request body from several concatenated streams
// The body is in the standard multipart/form-data format, as described in RFC 1867.
for (var paramName in MODELPARAMS) {
execRequestStream.append( StringToStream( "--" + boundary + "\n" ) );
execRequestStream.append( StringToStream( "Content-Disposition: form-data; name=\"param-" + paramName + "\"\n" ) );
execRequestStream.append( StringToStream( "\n" ) );
execRequestStream.append( StringToStream( MODELPARAMS[paramName] + "\n" ) );
}
execRequestStream.append( StringToStream( "--" + boundary + "\n" ) );
execRequestStream.append( StringToStream( "Content-Disposition: form-data; name=\"input\"; filename=\"input.mps.gz\"\n" ) );
execRequestStream.append( StringToStream( "Content-Type: application/octet-stream\n" ) );
execRequestStream.append( StringToStream( "\n" ) );
execRequestStream.append( fs.createReadStream(MODELFILE) );
execRequestStream.append( StringToStream( "\n" ) );
execRequestStream.append( StringToStream( "--" + boundary + "--\n" ) );
// Now, pipe our message body to the HTTP request
execRequestStream.pipe( request.post(requestOptions, function(err, response, body) {
if (err) {
rejectPromise(new Error(err));
}
else if (!response) {
rejectPromise(new Error("No response"));
}
else if (response.statusCode!=200) {
rejectPromise(new Error("Unexpected status code: "+response.statusCode+", body: "+body));
}
else {
resolvePromise(JSON.parse(body));
}
} ) );
});
}).then(function(executionResponse) {
// Note that the multipart-form-upload endpoint returns the execution status 'wrapped' in another structure.
// So check the status code & extract the status code from this.
// This would not be necessary if we were calling the REST endpoint for file upload.
if (executionResponse.status!=200) {
throw new Error("Execution request failed with status "+executionResponse.status+", entity: "+JSON.stringify(executionResponse.entity));
}
if (!executionResponse.entity) {
throw new Error("Execution request did not return execution status");
}
return executionResponse.entity
}).then(function(executionStatus) {
// executionStatus is a standard structure that contains various meta-data about an execution in
// the Xpress Executor service. It also contains relative paths to various REST resources
// relating to this execution - e.g. input, result, status, run log...
timers.execRequest.stop();
console.log("Execution accepted after "+timers.execRequest.elapsedSeconds+"s");
// Model will be executing asynchronously; repeatedly wait 1/4 second then re-fetch status until it
// is complete
console.log("Waiting for completion of execution");
timers.execInProgress = new WallTimer();
timers.execInProgress.start();
function waitForCompletion() {
if (executionStatus.status!=='NOT_COMPLETED' && executionStatus.status!=='NOT_LOADED') {
// Execution has finishd!
return Promise.resolve(executionStatus);
}
else {
// Wait 250ms
return delay(250).then(function() {
// Refresh executionInfo
return prequest({
method: 'GET',
url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.statusPath),
headers: {
"Authorization": 'Bearer '+authorizationToken
}
}).then(function(body) {
// Request returns updated executionStatus
executionStatus = body;
return waitForCompletion();
});
});
}
}
return waitForCompletion();
}).then(function(executionStatus) {
timers.execInProgress.stop();
console.log("Execution accepted after "+timers.execInProgress.elapsedSeconds+"s");
// Execution has completed; check that it was successful and display results as appropriate
console.log("Processing model results");
// In event of failure, echo the remote model status, exit code & run log to aid with troubleshooting
if (executionStatus.status!=='OK' || solverStatusCodes[executionStatus.exitCode]!=="OPTIMAL") {
// Execution failed for some reason
if (executionStatus.status!=='OK') {
console.log("Execution failed!");
console.log("Execution status: "+executionStatus.status);
console.log("Execution exit code: "+executionStatus.exitCode);
}
else {
console.log("Failed to solve to optimality!");
console.log("Solver status: "+solverStatusCodes[executionStatus.exitCode]);
}
console.log("");
console.log("Execution log:");
// Fetch the remote execution log as it will likely contain error messages from the model
return prequest({
method: 'GET',
url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.runLogPath),
headers: {
"Authorization": 'Bearer '+authorizationToken,
"Accept": 'text/plain'
}
}).then(function(runLog) {
console.log(runLog);
return executionStatus;
});
}
else {
timers.resultsRequest = new WallTimer();
timers.resultsRequest.start();
// Download results file
return prequest({
method: 'GET',
url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.resultPath),
headers: {
"Authorization": 'Bearer '+authorizationToken,
"Accept": 'application/octet-stream'
},
encoding: null // will return binary results file as a 'buffer'
}).then(function(solGzResultsBuffer) {
timers.resultsRequest.stop();
console.log("Results downloaded after "+timers.resultsRequest.elapsedSeconds+"s");
// We have downloaded the .slx file in compressed format
// Use zlib to decompress
console.log("Decompressing results to "+SOLUTIONFILE);
timers.uncompressResults = new WallTimer();
timers.uncompressResults.start();
var gunzip = zlib.createGunzip();
var bufferstream = new stream.PassThrough();
var slxfile = fs.createWriteStream(SOLUTIONFILE);
var gunzipStream = bufferstream.pipe(gunzip).pipe(slxfile);
return new Promise(function(resolve,reject) {
bufferstream.end(solGzResultsBuffer);
gunzipStream.on('finish',function() { resolve(executionStatus); });
gunzipStream.on('error', function(err) { reject(err); });
});
});
}
}).then(function(executionStatus) {
// Finally, delete execution from component, to free the resources it holds
console.log("Deleting execution from component");
timers.deleteRequest = new WallTimer();
timers.deleteRequest.start();
return prequest({
method: 'DELETE',
url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.statusPath),
headers: {
"Authorization": 'Bearer '+authorizationToken
}
});
}).then(function() {
timers.deleteRequest.stop();
console.log("execution deleted after "+timers.deleteRequest.elapsedSeconds+"s");
}).catch(function(err) {
if (err.statusCode) {
console.error("ERROR returned by Xpress Executor service: HTTP status code "+err.statusCode);
}
else {
console.error("ERROR encountered: "+err.message);
}
}); |