1
\$\begingroup\$

I want an API on a Flask/Gunicorn webserver to make a call to a separate, potentially long-running java application. If the java application finishes before the Gunicorn timeout, Flask should return the detailed response of the application. If the java application takes too long, I want to prempt any 504's and have Flask return a message saying that it is taking too long, but it will send the detailed response to the user's API when it is done. I have a current solution that appears to satisfy these requirements, and is implemented with multiprocessing and by setting an event in a way that isn't quite easy to understand and probably has issues that I cannot see.

There are three possible cases I can think of:

  1. The Java application finished before the specified timeout, and the detailed response is immediately returned by the main code.
  2. The Java application finished after the specified timeout, and the main code sends a response indicating that the detailed information will come later, which the process eventually does.
  3. The specified timeout was misjudged, and the webserver actually times out before the application finishes. The main code doesn't get a chance to return anything (except a 504). However the process is unaffected and still sends the detailed information.

What follows is basically the same implementation of my code, except I'm faking a few things to make it concise. Unfortunately because the tricks with event and time.sleep(1), it may have to be read twice to understand how it works. Please let me know if I can clarify anything.

import multiprocessing
import subprocess
import time

GUNICORN_TIMEOUT = 45  # 75% of actual timeout, 60 seconds

@app.route('/<uuid>')
def main(uuid):
    event = multiprocessing.Event()
    queue = multiprocessing.Queue()
    detailed_response = {'uuid': uuid}
    queue.put(detailed_response)

    process = multiprocessing.Process(target=_process, args=(queue, event,))
    process.start()
    # The Process will set the Event once the java app finishes
    flag = event.wait(timeout=GUNICORN_TIMEOUT)
    if flag:
        # Case 1: Application finished within time, so tell the process to quit
        event.clear()
    else:
        # Case 2: Application did not finish within time, let the user know
        return jsonify({"testing_complete": False}), 202

    ## Process finished before timeout, get detailed response and return it
    detailed_response = queue.get()
    return jsonify({"detailed_response": detailed_response}), 200

def _process(queue, event):
    detailed_response = queue.get()

    # Make call to Java aapp
    args = ['/path/to/app.sh', detailed_response['uuid']]
    p = subprocess.Popen([args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    detailed_response['stdout'], detailed_response['stderr'] = p.communicate()
    detailed_response['exit_code'] = p.returncode

    # Put detailed response on Queue so calling code can access it
    queue.put(detailed_response)

    # App has finished, let main code know by setting the event
    event.set()
    # Sleep to let main code react to the event getting set
    time.sleep(1)
    if not event.is_set():
        # Main code is still alive, so die
        return

    # Main code has timed out either by case 2 or case 3
    # Hit API with detailed response
    hit_api(detailed_response)
\$\endgroup\$
2
  • \$\begingroup\$ Can you edit the post to indicate clearly which parts of the code are real and which are fake? It's hard to review fake code. \$\endgroup\$ Jun 10, 2016 at 9:29
  • \$\begingroup\$ @GarethRees "fake" might have been a poor choice of words, but the Input from my caller is actually much more complicated than the uuid (but not relevant to the question). The call to hit_api isn't defined in this code, and the call to subprocess is not to /path/to/app.sh. \$\endgroup\$ Jun 10, 2016 at 19:11

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.