1

So I have a simple Flask app (no database connected) that needs to execute a function with arguments parsed via a form. This function processes large amounts of data (and takes a few minutes to do so), and it's divided in parts, after which, yields some strings. The idea is that while the function is running, these strings appear dinamically on a template. And I was able to make that, and when the app is hosted on a local server it functions correctly. But, when deployed to Google Cloud App Engine, it does not allow me to dinnamically output theses strings, instead, it waits till all the function was executed to output them. main.py

from flask import Flask, render_template, redirect, url_for, request, session, flash, Response
import requests
from jinja2 import Environment
from jinja2.loaders import FileSystemLoader

app = Flask(__name__)
app.secret_key = "123"

def my_function(arg1, arg2, arg3):
   #Does something and defines string1
   yield string1
   #Does something else and defines string2
   yield string2
   #Does something else and defines string3
   yield string3

@app.route('/', methods=['GET', 'POST'])
def home():
    if request.method == 'POST':
       arg1 = request.form.get('arg1')
       arg2 = request.form.get('arg2')
       arg3 = request.form.get('arg3')

       env = Environment(loader=FileSystemLoader('templates'))
       tmpl = env.get_template('result.html')
       return Response(tmpl.generate(result = my_function(arg1, arg2, arg3)))

   return render_template("index.html")

if __name__ == "__main__":
    app.run(debug=True)

index.html

<form method="POST">
   <input type="text" name="arg1">
   <input type="text" name="arg2">
   <input type="text" name="arg3">
   <input type="submit">
</form>

result.html

<div>
    {% for line in result %}
       <p>{{ line }}</p>
    {% endfor %}
</div>

app.yaml

runtime: python37

Terminal output after running $ python main.py and submitting form

"GET / HTTP/1.1" 200
"POST / HTTP/1.1" 200

and opens result.html while my_function() is running

Terminal output after deploying and running $ gcloud app logs read

https://i.stack.imgur.com/6lBIt.png

and stays on index.html till my_function() ends running and then opens result.html

matilde
  • 25
  • 4

2 Answers2

0

As @DazWilkin said - App Engine does not support streaming. In addition, App Engine has a deadline of 1 minute for each request i.e. each call to App Engine as part of your standard app has to return within a minute

With respect to your issue, you can

  1. Make an ajax call to a route on your end which kicks off a function (running on a different thread). This function runs and dumps output in a list
  2. Your Ajax call then initiates a polling routine i.e. every X seconds, make a call to the function in #1 above and return any values in the output bucket

It solves your problem but the results are not instantaneous - they are dependent on how often you poll your server.

If you really need real-time response and your application is significant or large then you'll have to look into PubSub but this won't run on app engine (as earlier mentioned). You'll have to go the route of Google Compute Engine (GCE)

NoCommandLine
  • 5,044
  • 2
  • 4
  • 15
0

I had the same problem: I needed to create a progress bar while the function is running, using Flask deployed in Google App Engine (GAE). I needed to work with a lot of data and I wanted to keep the user informed about the current state of the process (reducing the uncertainty of our clients).

Inspired by this Medium post: How to generate multiple progress bars in a Flask app from @Suresh Devalapalli and his GIT, I followed the same steps, and it worked perfectly in local Flask, but when I deployed this to GAE, not allowed me to dynamically see the outputs, instead, all the messages where shown together when the program reaches the 100%.

Reading more about the restrictions of GAE, I found that (as @DazWilkin said) App Engine waits until the response is complete before it is sent to the client. Moreover, GAE apparently can only manage one thread each time, so the requiered behavior would be impossible because you needeed at least another thread that keep asking about the state of the backend.

Then I found the answer of @Justin Beckwith in other StackOverflow question that inspired me to read more about "App Engine Flexible environment". I learned more from the official documentation, where you can see the comparison between the "Standard environment" and "Flexible environment". For operation and cost restrictions, please read the official documentation. The key feature that could solve this problem is Background threads: "Yes, with restrictions" for Standard environment and just "Yes" for Flexible environment. This lead me to try the @Suresh Devalapalli solution deployed in GAE Flexible Environment. For my surprise, it WORKED!! The same behaviour in local Flask and in GAE Flex.

So, @matilde, I will show my codes that could help you:

main.py

# DAVIDRVU - 2021-04-02
from flask import Flask, render_template, request, session, Response
import time

app = Flask(__name__)
app.secret_key = "123"

@app.route('/my_function')
def my_function():
    arg1 = session.get('arg1')
    arg2 = session.get('arg2')
    arg3 = session.get('arg3')

    def generate(arg1, arg2, arg3):
        #Does something and defines string1
        time.sleep(2)
        string1 = "arg1 = " + str(arg1)
        yield "data:" + str(string1) + "\n\n"
    
        #Does something else and defines string2
        time.sleep(2)
        string2 = "arg2 = " + str(arg2)
        yield "data:" + str(string2) + "\n\n"
    
        #Does something else and defines string3
        time.sleep(2)
        string3 = "arg3 = " + str(arg3)
        yield "data:" + str(string3) + "\n\n"

        yield "data:END_SIGNAL\n\n"

    resp = Response(generate(arg1, arg2, arg3), mimetype= 'text/event-stream', headers={'X-Accel-Buffering': 'no'})
    return resp

@app.route('/flask_post', methods=['POST'])
def flask_post():
    session['arg1'] = request.form.get('arg1')
    session['arg2'] = request.form.get('arg2')
    session['arg3'] = request.form.get('arg3')
    return render_template('result.html')

@app.route('/')
def root():
    return render_template('index.html')

if __name__ == "__main__":
    app.run(host='127.0.0.1', port=8080, debug=True)

Highlights from main.py:

  1. The use of session to pass variables from one Flask function to another.
  2. Explicit indicate the event-stream typ using: mimetype= 'text/event-stream'
  3. Explicit indicate that you don't want buffering the outputs using: headers={'X-Accel-Buffering': 'no'}. If you don't configure the headers, you don't get the messages until the function ends.

index.html

<!DOCTYPE html>
<html>
<head>
</head>

<body>
    <form action="{{ url_for('flask_post') }}" method="POST">
    <input type="text" name="arg1">
    <input type="text" name="arg2">
    <input type="text" name="arg3">
    <input type="submit">
    </form>
</body>
</html>

result.html

<!DOCTYPE html>
<html>
<head>
    <script>
        var source = new EventSource("/my_function");
        document.open();
        source.onmessage = function(event) {
            if(event.data == "END_SIGNAL"){
                source.close()
            }
            else{
                console.log(event.data);
                document.write("<p>" + event.data +"</p>");
            }
        }
        document.close();
    </script>
</head>
<body>
</body>
</html>

Highlights from result.html: The JavaScript calls "my_function" without parameters, and capture every message until the data come with the "END_SIGNAL". The parameters were obteined using session.get, which is a Flask work around.

requirements.txt

Flask
gunicorn

app.yaml

#################################################################################
# CASE 1: Standard environment AppEngine
#################################################################################
#runtime: python37

#entrypoint: gunicorn -b :$PORT main:app

#################################################################################
# CASE 2: Flexible Environment AppEngine
#################################################################################
runtime: python
env: flex
runtime_config:
    python_version: 3

manual_scaling:
  instances: 2
resources:
  cpu: 1
  memory_gb: 4
  disk_size_gb: 10  # Disk size must be between 10GB and 10240GB

entrypoint: gunicorn -b :$PORT main:app
#################################################################################

Highlights from app.yaml: This is the most important part of this implementation. I personally tested both Environmets and observe the different behaviours:

  1. CASE 1 (commented lines), for the Standard environment deployment: When the function ends, the outputs appears.

  2. CASE 2, for the Flexible Environment deployment: I finally obtained the desired behaviour, showing the messages from the backend to HTML in "real time" while the function is still working, allowing you to show the current state from the backend. Note the parameters required for each Environment are differents and the "deploy time" is longer in the Flex environment.

I uploaded the codes to my GIT

I hope this could help you!