0

I have set up a website that uses Flask, nginx and uWSGI.

The website needs to always show the most up to date information. I have been testing that by changing the theme of my website. I made this theme work by storing the hex value in a JSON file and then retrieving that when my app.py is first called. I update the global theme_colour variable whenever they save the config. I then pass theme_colour into each render_template and within the HTML I have a style tag that contains something like this

h1 {
    color: {{ theme_colour }}
}

My problem is that when I update the theme, it should instantly change all pages but it does not. When I update it on the config page it automatically reloads the page and the change works there, but when I navigate to another page it's using the old colour. Sometimes refreshing the page fixes it, but then randomly swapping between pages will often result in it swapping back to the old colour for some reason. Clearing the cache and refreshing it will fix it on that page, but then navigating to another page and back it often swaps back to the old colour.

I can't figure out what is going on here. I didn't have any issues with the theme before I hosted the website properly (it was just on the dev server before). I tried to eliminate caching like this but it clearly hasn't worked:


server {
        listen 80 default_server;
        server_name mydomain.com www.mydomain.com;

        location / {
                include uwsgi_params;
                uwsgi_pass unix:/home/ubuntu/test/test.sock;
                # kill cache
                sendfile off;
                add_header Last-Modified $date_gmt;
                add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
                if_modified_since off;
                expires off;
                etag off;
                proxy_no_cache 1;
                proxy_cache_bypass 1;
        }

        return 301 https://$server_name$request_uri;
}

server {
        listen 443 ssl default_server; # managed by Certbot
        ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot#
        server_name mydomain.com www.mydomain.com;

        location / {
                include uwsgi_params;
                uwsgi_pass unix:/home/ubuntu/test/test.sock;
                # kill cache
                sendfile off;
                add_header Last-Modified $date_gmt;
                add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
                if_modified_since off;
                expires off;
                etag off;
                proxy_no_cache 1;
                proxy_cache_bypass 1;
        }
}

Whenever I send a GET request it says the response headers contain Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0 so I'm not sure that this is the problem.

Here is all of my theme_colour code:

@app.route("/index")
def load_index():
    if check_logged_in(session, 0):  # if user is logged in, doesn't require elevated privileges
        return render_template("index.html", theme_colour=theme_colour)  # logged in
    else:
        return redirect("/login")  # not logged in


@app.route("/config", methods=["POST", "GET"])
def load_config():
    if check_logged_in(session, 2):  # if user is logged in, does require elevated privileges
        if request.method == "POST":  # if user clicks update button
            global theme_colour  # make theme colour accessible to all pages
            config = list(request.form.values())  # get input box values
            save_config(config)  # save values to json
            theme_colour = update_theme()  # find theme colour and update global variable
        return render_template("config.html", theme_colour=theme_colour)
    else:
        return abort(403)  # forbidden if not logged in


def update_theme():  # get current theme colour
    with open("config.json", "r") as json_file:
        data = json.load(json_file)
    theme_colour = data["colour"]
    return theme_colour


theme_colour = update_theme()

I feel like I am missing something here. Does anyone have any ideas?

Thanks.

Jack P
  • 469
  • 1
  • 6
  • 16
  • The stack you describe should not be doing any caching in the first place unless explicitly instructed to do so. Can you show us the full Nginx, uWSGI and Flask app configurations? – AKX Mar 22 '19 at 11:01
  • @AKX Sorry I just set this up yesterday I'm still working it out, which files do you mean exactly? – Jack P Mar 22 '19 at 11:04
  • Practically the full Nginx config (not just the server blocks), the uWSGI config file / command line, and however you're setting up the `app` object in Flask. – AKX Mar 22 '19 at 11:21
  • in your `/config` route you create a local variable `theme_colour` by calling `update_theme()`, whereas in your `/index` route you rely on the global `theme_colour` variable (and I assume you rely on the global variable in your other routes too). Globals notoriously don’t play nice with multiprocess web servers. If you call `update_theme` in each route instead of using the global does it solve it? – SuperShoot Mar 22 '19 at 11:22
  • @SuperShoot Multithreaded web servers they do work with, multiprocessed (which most Python app servers are) are a different story. – AKX Mar 22 '19 at 11:22
  • Thanks @AKX have edited. – SuperShoot Mar 22 '19 at 11:24
  • @SuperShoot I didn't create a local variable, I used `global theme_colour`. Yes as I stated in my edit it seems to be doing the job, just unfortunate as it's a bit less efficient. – Jack P Mar 22 '19 at 11:34
  • Yes, you are referencing the global but the key takeaway is that you are calling the `update_theme()` function locally.. but you’ve already worked that out! – SuperShoot Mar 22 '19 at 11:41

2 Answers2

1

Ah yeah, to follow up on what @SuperShoot said in the comments: You can't reliably use globals in most web server contexts. Most of the time you'll have more than one process serving requests (or at least one process that may die at any time and be reborn), and global variables wouldn't be shared between those processes.

To reliably share state between requests, you'll need to use some external storage for it (unless you can store the state within the session – in which case it won't be shared between different clients).

I would suggest hooking up Redis for this, but there are other options, such as

  • the file system (as flat files (e.g. /tmp/myapp/theme.txt) or shelve or similar)
  • another cache server such as Memcache
  • a database (SQLite, some other SQL, something else)
  • uWSGI's Cache subsystem (though it's obviously uWSGI only)
  • uWSGI's SharedArea subsystem (very low level and not recommended, but it's there)

The idea then would be to load the shared value from the backend whenever it's needed (or for every request).


Either way, I'm pretty confident you can get rid of all that caching configuration in Nginx – Nginx won't cache things by itself unless told to (either by its configuration, or possibly by headers sent by your app).

AKX
  • 152,115
  • 15
  • 115
  • 172
  • I think I'll probably just stick with calling the function on every page load, but thanks. It's not a large scale project so I don't think it will be an issue. – Jack P Mar 22 '19 at 11:35
  • And I have removed the caching config and it still seems to be working, thanks. – Jack P Mar 22 '19 at 11:38
0

I fixed it by setting it to call the function on every page load, i.e. return render_template("config.html", theme_colour=update_theme()). This is less efficient but suits my needs.

I was also able to remove all the caching config as that didn't seem necessary, thanks @AKX.

Jack P
  • 469
  • 1
  • 6
  • 16