9.3 Asynchronous

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

Since the stateless OptServerLight forgets a problem shortly after solving it, the full OptServer should be considered for serious asynchronous optimization applications where the solution is to be retrieved, possibly, a long and unspecified time after the job’s submission.

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

Assuming that an HTTP/HTTPS connection to the OptServer was established, we first submit a problem using submit. The file format is passed in the Content-Type header.

Listing 9.10 Submit a problem.
            # POST problem data
            submit = s.post(URL + "/api/v1/submit", 
                            data = probdata,
                            headers = { "Content-Type" : intype },
                            verify = verify )

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 9.11 Start solving the submission.
                # Request the server to solve the problem in the background
                solve = s.get(URL + "/api/v1/solve-background", 
                              headers = { "X-Mosek-Job-Token" : token },
                              verify = verify )

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 9.12 Polling for the solution.
            pollCount += 1
            sol = s.get(URL + "/api/v1/solution",
                        headers = { "X-Mosek-Job-Token" : token ,
                                    "Accept" : outtype },
                        verify = verify )

            if sol.status_code == requests.codes.no_content:
                # Solution no yet available
                print(f"Solution not available in poll {pollCount}, continuing")
                time.sleep(1.0)

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

Listing 9.13 Retrieving the solution when available.
            elif sol.status_code == requests.codes.ok:
                # Solution is available
                solved = True
                if outtype in ["application/json", "application/x-mosek-jtask"]:
                    solution = json.loads(sol.text)
                else:
                    solution = sol.text
                res = sol.headers["X-Mosek-Res-Code"]
                trm = sol.headers["X-Mosek-Trm-Code"]

Stopping the solver

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

Listing 9.14 Stopping the solver.
            # After too many attempts we indicate the solver to stop
            if not solved and pollCount >= maxPolls:
                s.get(URL + "/api/v1/break",
                      headers = { "X-Mosek-Job-Token" : token },
                      verify = verify )

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 9.15 Retrieving log output.
            # Get the log from the last call until now
            log = s.get(URL + "/api/v1/log" + f"?offset={logOffset}", 
                    headers = { "X-Mosek-Job-Token" : token },
                    verify = verify )
            print(log.text)
            logOffset += len(log.text)

Complete code

The full example is shown below.

Listing 9.16 How to submit a job and solve the problem asynchronously. Click here to download.
    # Create a connection
    token = ""
    with requests.Session() as s:
        with open(infile,'rb') as probdata:
            # POST problem data
            submit = s.post(URL + "/api/v1/submit", 
                            data = probdata,
                            headers = { "Content-Type" : intype },
                            verify = verify )
            if submit.status_code == requests.codes.ok:
                token = submit.headers['X-Mosek-Job-Token']
                print("Submit: success")

                # Request the server to solve the problem in the background
                solve = s.get(URL + "/api/v1/solve-background", 
                              headers = { "X-Mosek-Job-Token" : token },
                              verify = verify )
                if solve.status_code not in [requests.codes.ok, requests.codes.no_content]:
                    print(f"Error initiating solve, status = {solve.status_code}")
                    sys.exit(-1)
            else:
                print(f"Error submitting job, status = {submit.status_code}")
                sys.exit(-1)

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

    with requests.Session() as s:
        while not solved:
            pollCount += 1
            sol = s.get(URL + "/api/v1/solution",
                        headers = { "X-Mosek-Job-Token" : token ,
                                    "Accept" : outtype },
                        verify = verify )

            if sol.status_code == requests.codes.no_content:
                # Solution no yet available
                print(f"Solution not available in poll {pollCount}, continuing")
                time.sleep(1.0)
            elif sol.status_code == requests.codes.ok:
                # Solution is available
                solved = True
                if outtype in ["application/json", "application/x-mosek-jtask"]:
                    solution = json.loads(sol.text)
                else:
                    solution = sol.text
                res = sol.headers["X-Mosek-Res-Code"]
                trm = sol.headers["X-Mosek-Trm-Code"]
            else:
                print(f"Error querying for solution, status = {sol.status_code}")

            # After too many attempts we indicate the solver to stop
            if not solved and pollCount >= maxPolls:
                s.get(URL + "/api/v1/break",
                      headers = { "X-Mosek-Job-Token" : token },
                      verify = verify )

            # Get the log from the last call until now
            log = s.get(URL + "/api/v1/log" + f"?offset={logOffset}", 
                    headers = { "X-Mosek-Job-Token" : token },
                    verify = verify )
            print(log.text)
            logOffset += len(log.text)

            if solved:
                print(f"Solution: {solution}")
                print(f"Response code:    {res}")
                print(f"Termination code: {trm}")