I chose to use a server-side session management with Flask using Flask-Session.
I store the data using filesystem and as expected, these files are stored under a /flask_session folder in my config directory.
Here is how I set this up in my __init__.py
# __init__.py
from flask_session import Session
[...]
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SECRET_KEY'] = config.SECRET_KEY
sess = Session()
sess.init_app(app)
As expected, session files generated & stored under /flask_session
▾ flask_session/
1695e5cbf9b4edbbbb82a8ef1fad89ae
192761f7ce8e3cbf3ca11665133b7794
2029240f6d1128be89ddc32729463129
...
Question is: Are these files automatically removed by flask_session after a specific amount of time (ie. as the session stored client-side)? If yes, is it possible to decrease/increase this timing?
As Danila Ganchar commented, using PERMANENT_SESSION_LIFETIME allows to control the session expiration time.
Flask-Session use the same builtin config than Flask itself (related to session). From Flask-Session doc:
The following configuration values are builtin configuration values
within Flask itself that are related to session. They are all
understood by Flask-Session, for example, you should use
PERMANENT_SESSION_LIFETIME to control your session lifetime.
Example:
# __init__.py
from flask_session import Session
from datetime import timedelta
app.config['SESSION_PERMANENT'] = True
app.config['SESSION_TYPE'] = 'filesystem'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=5)
# The maximum number of items the session stores
# before it starts deleting some, default 500
app.config['SESSION_FILE_THRESHOLD'] = 100
app.config['SECRET_KEY'] = config.SECRET_KEY
sess = Session()
sess.init_app(app)
Related
I'm trying to find a kind of memory leak in my flask REST-API for a few days now without any relevant progress.
I have a flask REST-API using a mysql database (packages like SQLAlchemy, connexion and marshmallow). It is available via a docker container which have a base image from alpine:latest.
The main problem i have: with every request to the REST-API the memory usage of the docker container increases and the memory is not released. The API do not cache the results.
Here is the code from the server.py (the main program of the RESt-API):
"""
Main module of the server file
"""
# 3rd party moudles
# local modules
import config
# Get the application instance
connex_app = config.connex_app
# Read the swagger.yml file to configure the endpoints
connex_app.add_api("swagger_2.0.yml")
# create a URL route in our application for "/"
#connex_app.route("/")
def home():
return None
if __name__ == "__main__":
connex_app.run(debug=True)
and the config file:
import os
import connexion
from flask_cors import CORS
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from memory_profiler import memory_usage
basedir = os.path.abspath(os.path.dirname(__file__))
# Create the Connexion application instance
connex_app = connexion.App(__name__, specification_dir=basedir)
# Get the underlying Flask app instance
app = connex_app.app
CORS(app)
# Configure the SQLAlchemy part of the app instance
app.config['SQLALCHEMY_ECHO'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:somepassword#someHostId/sponge"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
#app.after_request
def add_header(response):
#response.cache_control.no_store = True
if 'Cache-Control' not in response.headers:
response.headers['Cache-Control'] = 'max-age=0'
print(memory_usage(-1, interval=.2, timeout=1), "after request")
return response
# Create the SQLAlchemy db instance
db = SQLAlchemy(app)
# Initialize Marshmallow
ma = Marshmallow(app)
An example for an endpoint you can see here:
from flask import abort
import models
def read(disease_name=None):
"""
This function responds to a request for /sponge/dataset/?disease_name={disease_name}
with one matching entry to the specifed diesease_name
:param disease_name: name of the dataset to find (if not given, all available datasets will be shown)
:return: dataset matching ID
"""
if disease_name is None:
# Create the list of people from our data
data = models.Dataset.query \
.all()
else:
# Get the dataset requested
data = models.Dataset.query \
.filter(models.Dataset.disease_name.like("%" + disease_name + "%")) \
.all()
# Did we find a dataset?
if len(data) > 0:
# Serialize the data for the response
return models.DatasetSchema(many=True).dump(data).data
else:
abort(404, 'No data found for name: {disease_name}'.format(disease_name=disease_name))
I tried to find the memory leak within the code with the memory_profiler tool, but since the same behavior (increasing memory usage of the docker container at each request) can be observed at each REST-API endpoint.
Can anyone explain what is happening oder have an idea of how i can fix the caching problem.
Problem is fixed. Actually it was no problem. The docker stats memory usage increases due to implementation of python. If the rest-api request is multiple GB big, then python allocates a certain percentage of that used memory and do not free it immidiatly. So the peaks at 500 GB were after a relly great answer. I added to the API endpoints a fixed limit and and hint for the user if he exceeds this limit he should download the whole database as a zip fornat and work locally with it.
I've been thinking about the factory pattern for WSGI applications, as recommended by the Flask docs, for a while now. Specifically about those functions usually being shown to make use of objects that have been created at module import time, like db in the example, as opposed to having been created in the factory function.
Would the factory function ideally create _everything_ anew or wouldn't that make sense for objects like the db engine?
(I'm thinking cleaner separation and better testability here.)
Here is some code, where I'm trying to accomplish creating all needed objects for the wsgi app. in its factory function.
# factories.py
def create_app(config, engine=None):
"""Create WSGI application to be called by WSGI server. Full factory function
that takes care to deliver entirely new WSGI application instance with all
new member objects like database engine etc.
Args:
config (dict): Dict to update the wsgi app. configuration.
engine (SQLAlchemy engine): Database engine to use.
"""
# flask app
app = Flask(__name__) # should be package name instead of __name__ acc. to docs
app.config.update(config)
# create blueprint
blueprint = ViewRegistrationBlueprint('blueprint', __name__, )
# request teardown behaviour, always called, even on unhandled exceptions
# register views for blueprint
from myapp.views import hello_world
# dynamically scrapes module and registers methods as views
blueprint.register_routes(hello_world)
# create engine and request scoped session for current configuration and store
# on wsgi app
if (engine is not None):
# delivers transactional scope when called
RequestScopedSession = scoped_session(
sessionmaker(bind=engine),
scopefunc=flask_request_scope_func
)
def request_scoped_session_teardown(*args, **kwargs):
"""Function to register and call by the framework when a request is finished
and the session should be removed.
"""
# wrapped in try/finally to make sure no error collapses call stack here
try:
RequestScopedSession.remove() # rollback all pending changes, close and return conn. to pool
except Exception as exception_instance:
msg = "Error removing session in request teardown.\n{}"
msg = msg.format(exception_instance)
logger.error(msg)
finally:
pass
app.config["session"] = RequestScopedSession
blueprint.teardown_request(request_scoped_session_teardown)
# register blueprint
app.register_blueprint(blueprint)
return app
def create_engine(config):
"""Create database engine from configuration
Args:
config (dict): Dict used to assemble the connection string.
"""
# connection_string
connection_string = "{connector}://{user}:{password}#{host}/{schema}"
connection_string = connection_string.format(**config)
# database engine
return sqlalchemy_create_engine(
connection_string,
pool_size=10,
pool_recycle=7200,
max_overflow=0,
echo=True
)
# wsgi.py (served by WSGI server)
from myapp.factories import create_app
from myapp.factories import create_engine
from myapp.configuration.config import Config
config = Config()
engine = create_engine(config.database_config)
app = create_app(config.application_config, engine=engine)
# conftest.py
from myapp.factories import create_app
from myapp.factories import create_engine
from myapp.configuration.config import Config
#pytest.fixture
def app():
config = TestConfig()
engine = create_engine(config.database_config)
app = create_app(config.application_config, engine=engine)
with app.app_context():
yield app
As you also tagged this with sanic I'll answer with that background. Sanic is async and thus relies on an event loop. An event loop is a resource and thus must not be shared between tests but created anew for each one. Hence, the database connection etc also need to be created for each test and cannot be re-used as it is async and depends on the event loop. Even without the async nature it would be cleanest to create db connections per test because they have state (like temp tables).
So I ended up with a create_app() that creates everything which allows me to create an arbitrary number of independent apps in a test run. (To be honest there are some global resources like registered event listeners, but tearing those down is easy with py.test factories.) For testability I'd try to avoid global resources that are created on module import. Although I've seen differently in big and successful projects.
That's not really a definite answer, I know...
What is the easiest way to have a server-side session variable in Flask?
Variable value:
A simple string
Not visible to the client (browser)
Not persisted to a DB -- simply vanishes when the session is gone
There is a built-in Flask session, but it sends the session data to the client:
session["secret"] = "I can see you"
The data is Base64 encoded and sent in a cryptographically signed cookie, but it is still trivial to read on the client.
In many frameworks, creating a server-side session variable is a one-liner, such as:
session.secret = "You can't see this"
The Flask solutions I have found so far are pretty cumbersome and geared towards handling large chunks of data. Is there a simple lightweight solution?
I think the Flask-Session extension is what you are looking for.
Flask-Session is an extension for Flask that adds support for Server-side Session to your application.
From the linked website:
from flask import Flask, session
from flask_session import Session # new style
# from flask.ext.session import Session # old style
app = Flask(__name__)
# Check Configuration section for more details
SESSION_TYPE = 'redis'
app.config.from_object(__name__)
Session(app)
#app.route('/set/')
def set():
session['key'] = 'value'
return 'ok'
#app.route('/get/')
def get():
return session.get('key', 'not set')
This answer is from June 2020 for flask-session 0.3.2.
The documentation is here.
There are several available SESSION_TYPESs. filesystem is the most straightforward while you're testing. The expectation is you already have a Redis, database, etc. setup if you are going to use the other SESSION_TYPEs. Section on SESSION_TYPE and requirements
null: NullSessionInterface (default)
Redis: RedisSessionInterface
Memcached: MemcachedSessionInterface
filesystem: FileSystemSessionInterface
MongoDB: MongoDBSessionInterface
SQLAlchemy: SqlAlchemySessionInterface
Code example from the documentation. If you go to /set/ then the session['key'] is populated with the word 'value'. But if you go to /get/ first, then `session['key'] will not exist and it will return 'not set'.
from flask import Flask, session
from flask_session import Session
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'filesystem'
#personal style preference compared to the first answer
Session(app)
#app.route('/set/')
def set():
session['key'] = 'value'
return 'ok'
#app.route('/get/')
def get():
return session.get('key', 'not set')
I have this route in a flask app:
#APP.route('/comparisons', methods=['POST'])
def save_comparison():
if 'comparisons' not in session or not session['comparisons']:
session['comparisons'] = []
entity_id = request.form.get('entity_id')
session['comparisons'].append(entity_id)
session.modified = True
return entity_id
APP.secret_key = 'speakfriend'
This adds entity_ids to session['comparisons'] as expected. However, when I refresh the page, the session object does not have a 'comparisons' property anymore, so the list of comparisons is empty. What am I missing?
Update:
I left out what I didn't know was the important information. The vue app also makes calls to a flask api, which sets its own session. The SECRET_KEYs were different. So, when there was an api call between webserver calls (or visa versa) the session from one application would be replaced by the session from the other. Neither was mutually intelligible (different SECRET_KEYs). Since these are always deployed together using docker-compose the solution was to use a common an env variable to pass the same secret to both.
I am using Flask kvsession to avoid replay attacks, as the client side cookie based session used by Flask-login are prone to it.
Eg: If on /index page your cookie in the header is set for your app header like
myapp_session : 'value1'
and if you navigate to /important page you will get a new header like
myapp_session : 'value2' so if a hacker gets 'value1' he can perform replay attacks and misuse it, as it is never invalidated.
To solve this I am using flask-kvsession which stores the session cookie header value in a cache or some backend. SO basically only one myapp_session is generated and invalidated when you logout. But the problem is :-
__init__.py
from simplekv.memory.redisstore import RedisStore
import redis
store = RedisStore(redis.StrictRedis())
#store = memcache.Client(['127.0.0.1:11211'], debug =0)
store.ttl_support = True
app = create_app(__name__)
current_kvsession = KVSessionExtension(store, app)
If you look at the cleanup_session part of the code for kv-session
http://pythonhosted.org/Flask-KVSession/#flask_kvsession.KVSessionExtension.cleanup_sessions
It only deletes the expired sessions. But If I want to explicitly delete the value for the current myapp_session for a particular user on logout, how do I do that?
#app.before_request
def redirect_if_logout():
if request.path == url_for('logout'):
for key in app.kvsession_store.keys():
logger.debug(key)
m = current_kvsession.key_regex.match(key)
logger.debug('found %s', m)
app.kvsession_store.delete(key)
But this deletes all the keys as I don`t know what the unique key for the current session is.
Q2. Also, how to use memcache instead of redis as it doesn`t have the app.kvsession_store.keys() function and gives i/o error.
I think I just figured the 1st part of your question on how you can delete the specific key on logout.
As mentioned in the docs:
Internally, Flask-KVSession stores session ids that are serialized as
KEY_CREATED, where KEY is a random number (the sessions “true” id) and
CREATED a UNIX-timestamp of when the session was created.
Sample cookie value that gets created on client side (you can check with that cookie manager extenion for firefox):
c823af88aedaf496_571b3fd5.4kv9X8UvyQqtCtNV87jTxy3Zcqc
and session id stored in redis as key:
c823af88aedaf496_571b3fd5
So on logout handler, you just need to read the cookie value, split it and use the first part of the string:
Sample Code which worked for me:
import redis
from flask import Flask
from flask_kvsession import KVSessionExtension
from simplekv.memory.redisstore import RedisStore
store = RedisStore(redis.StrictRedis())
app = Flask(__name__)
KVSessionExtension(store, app)
#Logout Handler
#app.route('/logout', methods=['GET'])
def logout():
#here you are reading the cookie
cookie_val = request.cookies.get('session').split(".")[0]
store.delete(cookie_val)
and since you have added ttl_support:
store.ttl_support = True
It will match the TTL(seconds) value from permanent_session_lifetime, if you have set that in config file or in the beginning of your app.py file.
For example, in my application I have set in the beginning of app.py file as:
session.permanent = True
app.permanent_session_lifetime = timedelta(minutes=5)
now, when I logout, it deletes the key in redis but it will not be removed until TTL for that turns to 0 from 300 (5 Min as mentioned in permanent_session_lifetime value ).
If you want to remove it from redis immediately, for that you can manually change the app.permanent_session_lifetime to 1 second, which will in turn change TTL for redis.
import redis
import os
from flask import Flask
from flask_kvsession import KVSessionExtension
from simplekv.memory.redisstore import RedisStore
store = RedisStore(redis.StrictRedis())
app = Flask(__name__)
KVSessionExtension(store, app)
#Logout Handler
#app.route('/logout', methods=['GET'])
def logout():
cookie_val = request.cookies.get('session').split(".")[0]
app.permanent_session_lifetime = timedelta(seconds=1)
store.delete(cookie_val)
Using the above code, I was able to thwart session replay attacks.
and solution to your 2nd question:
3 possible mistakes that I can see are:
1: In the beginning of your code you have created:
store = RedisStore(redis.StrictRedis())
but in the loop you are using it as kvsession_store instead of just store:
app.kvsession_store.keys()
To use it without any errors/exceptions you can use it as store.keys() instead of app.store.keys():
from flask_kvsession import KVSessionExtension
from simplekv.memory.redisstore import RedisStore
store = RedisStore(redis.StrictRedis())
for key in store.keys():
print key
store.delete(key) is not deleting the all keys, you are running it inside the loop which is one by one deleting all keys.