7.2 Asynchronous Problem Submission

This tutorial demonstrates most features of the OptServer API, that is submitting a problem, polling for solution, retrieving the solution, breaking the solver and retrieving the log output.

For the purpose of the tutorial we assume that the problem to be solved is read from a file, and the solutions will be saved to a file, i.e. we don’t go into the logic which sets up the problem and interprets the solution. See file formats for specifications of file formats.

Starting the solver

If using authentication, the access token will always be passed in the X-Mosek-Access-Token header. Assuming that an HTTP/HTTPS connection to the OptServer was established, we first submit a problem using submit. We can provide a name for easier identification of the problem in the Web GUI. The file format is passed in the Content-Type header.

Listing 7.7 Submit a problem.
            # POST problem data
            con.request('POST', '/api/submit?jobname=' + jobname, 
                                probdata, 
                                headers = dict(headers, **{"Content-Type": "application/x-mosek-{}".format(intype)}))
            resp = con.getresponse()
            check_status(resp)
            # Recover a token identifying the job
            token = resp.read().decode('ascii')      

The response contains a token used to identify the job in future requests. Note that this operation is identical to the synchronuous case. If no errors have occurred, we use solve-background to initiate solving the problem identified by the token:

Listing 7.8 Start solving the submission.
            con.request("GET","/api/solve-background?token=" + token, headers = headers)
            resp = con.getresponse()

The calling program regains control immediately.

Waiting for and retrieving the solution

We can now periodically start polling for the solution via solution. We set the Accept header to indicate expected solution format. If the response is empty then the solution is not yet available:

Listing 7.9 Polling for the solution.
        pollCount += 1
        print("GET /api/solution")
        con.request("GET", "/api/solution?token=" + token, 
                           headers = dict(headers, **{"Accept": outtype}))
        resp = con.getresponse()
        check_status(resp)

        # Is the solution available?
        if resp.status == http.client.NO_CONTENT:
            print("Solution not available in poll %d, continuing" % pollCount)
            time.sleep(1.0)

When the response becomes non-empty we can retrieve the solution:

Listing 7.10 Retrieving the solution when available.
        elif resp.status == http.client.OK:    
            solved = True
            res = resp.getheader('X-Mosek-Res-Code',None)
            trm = resp.getheader('X-Mosek-Trm-Code',None)
            print("\tMOSEK response: %s" % res)
            print("\t      trm resp: %s" % trm)
        
            print("Solution (as plain text):")
            print(resp.read().decode('ascii', errors = 'ignore'))

Stopping the solver

At some point we can decide that the optimization should be stopped. That can be done with break.

Listing 7.11 Stopping the solver.
        if not solved and pollCount >= maxPolls:
            con = openConnection(host, port, useHttps)
            print("GET /api/break")
            con.request("GET","/api/break?token=" + token, headers = headers)
            resp = con.getresponse()
            check_status(resp)
            con.close()        

Note that the solver need not break immediately, in particular it can enter a few more loops of checking for solution. The MOSEK termination code in this case will be MSK_RES_TRM_USER_CALLBACK.

Retrieving the log

The log output from the solver can be retrieved gradually in each polling loop. The caller needs to keep track of how much of the log was already read and provide it as an offset in a call to log.

Listing 7.12 Retrieving log output.
        con = openConnection(host, port, useHttps)
        print("GET /api/log")
        con.request("GET","/api/log?token={0}&offset={1}".format(token, logOffset), headers = headers)
        resp = con.getresponse()
        check_status(resp)
        # Show the latest log entries
        lastLog = resp.read().decode('ascii', errors = 'ignore')
        print(lastLog)
        # Update the log offset by the size received
        logOffset += len(lastLog)
        con.close()        

Complete code

The full example is shown below.

Listing 7.13 How to submit a job and solve the problem asynchronously. Click here to download.
import http.client
import sys, time
try:
    import ssl
except:
    pass

# A debug method which prints out the HTTP(S) response 
# and exits in case of error
def check_status(resp):
    print('\tHTTPResponse: %s / %s' % (resp.status,resp.reason))
    for k,v in  resp.getheaders():
        print('\t%s: %s' % (k,v))
            
    if resp.status not in [http.client.OK, http.client.NO_CONTENT]:
        raise Exception('An error in connection')

# A helper method which opens a connection
def openConnection(host, port, useHttps):
    if useHttps:
        return http.client.HTTPSConnection(host, port, context=ssl._create_unverified_context())
    else:
        return http.client.HTTPConnection(host, port)

if __name__ == '__main__':
    try:
        # OptServer address
        host, port = sys.argv[1], int(sys.argv[2])
        # Protocol (HTTP/HTTPS)
        useHttps = (sys.argv[3] == "HTTPS")
        # Name of file with input data
        probfile = sys.argv[4]        
        # Input and output file type
        intype, outtype = sys.argv[5], sys.argv[6]
        # Number of solution polls
        maxPolls = int(sys.argv[7])
        # Jobname (for demonstration)
        jobname = sys.argv[8] 
        # Authentication token
        headers = {}
        if len(sys.argv) == 10:
            headers = {"X-Mosek-Access-Token": sys.argv[9]}
    except:
        print("Usage  : python3 test_async.py host            port  protocol  probfile intype outtype    maxPolls jobname     [accestoken]")
        print("Example: python3 test_async.py solve.mosek.com 38000 HTTPS     lo1.mps  mps    text/plain 5        SimpleTask  ...")
        sys.exit(1)

    token = ""
    
    # Create a connection for problem submission
    con = openConnection(host, port, useHttps)
    try:
        with open(probfile,'rb') as probdata:
            ## Submit job
            print('POST /api/submit')
            # POST problem data
            con.request('POST', '/api/submit?jobname=' + jobname, 
                                probdata, 
                                headers = dict(headers, **{"Content-Type": "application/x-mosek-{}".format(intype)}))
            resp = con.getresponse()
            check_status(resp)
            # Recover a token identifying the job
            token = resp.read().decode('ascii')      
            
            ## Start solving end close connection
            print("GET /api/solve-background")
            con.request("GET","/api/solve-background?token=" + token, headers = headers)
            resp = con.getresponse()
            check_status(resp)
    finally:
        con.close()
        print("Submit connection closed")

    # Begin waiting for the solution
    solved = False
    pollCount = 0
    logOffset = 0

    while not solved:
        con = openConnection(host, port, useHttps)
        pollCount += 1
        print("GET /api/solution")
        con.request("GET", "/api/solution?token=" + token, 
                           headers = dict(headers, **{"Accept": outtype}))
        resp = con.getresponse()
        check_status(resp)

        # Is the solution available?
        if resp.status == http.client.NO_CONTENT:
            print("Solution not available in poll %d, continuing" % pollCount)
            time.sleep(1.0)
        elif resp.status == http.client.OK:    
            solved = True
            res = resp.getheader('X-Mosek-Res-Code',None)
            trm = resp.getheader('X-Mosek-Trm-Code',None)
            print("\tMOSEK response: %s" % res)
            print("\t      trm resp: %s" % trm)
        
            print("Solution (as plain text):")
            print(resp.read().decode('ascii', errors = 'ignore'))
        con.close()

        # After too many tries we indicate the solver to stop
        if not solved and pollCount >= maxPolls:
            con = openConnection(host, port, useHttps)
            print("GET /api/break")
            con.request("GET","/api/break?token=" + token, headers = headers)
            resp = con.getresponse()
            check_status(resp)
            con.close()        

        # Demonstrate how to retrieve the last part of solver log
        con = openConnection(host, port, useHttps)
        print("GET /api/log")
        con.request("GET","/api/log?token={0}&offset={1}".format(token, logOffset), headers = headers)
        resp = con.getresponse()
        check_status(resp)
        # Show the latest log entries
        lastLog = resp.read().decode('ascii', errors = 'ignore')
        print(lastLog)
        # Update the log offset by the size received
        logOffset += len(lastLog)
        con.close()