Store sessions in individual files within a directory.
There was a problem with the existing code (adopted from the Python 2 version), which lead to an “EOFError: Ran out of input” exception
The code in save_session() did:
f = open(filename, 'wb')
…which immediately made the file zero bytes long. You can try this out in 2
terminals (where s
is some dummy class with s.id
as the file-name) with one doing:
import pickle
pickle.dump(s, f, 4)
f.close()
f = open(s.id, 'wb')
If in the other terminal you do:
f = open(s.id, 'rb')
o = pickle.load(f)
Traceback (most recent call last):
...EOFError: Ran out of input
This is not entirely unexpected BUT the code in load_session()
:
f = open(filename, 'rb')
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
…could get the shared lock (LOCK_SH) after save_session() performed
the open() but BEFORE save_session()
got a chance to get the exclusive lock:
f = open(filename, 'wb')
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
(You can try it in the older code by quickly refreshing a browser calling a Quixote server.)
What happened appears to have been:
As save_session() truncated the file and then waited for an exclusive lock, we had to have load_session() check for a zero-sized file. If it has one, then save_session() has just created (or re-created) it and we should let go and try again.
It turns out that, during testing, one can get at EOFError
from pickle anyway, so a check for that was added too.
import sys
if sys.version_info < (3,4,0):
sys.stderr.write("You need python 3.4.0 or later to run this script\n")
exit(1)
import fcntl
import os
import os.path
from pickle import dump, load
from session3.store.SessionStore import SessionStore
import time
SLEEPY_TIME = 0.1
Store sessions in individual files within a directory.
class DirectorySessionStore(SessionStore):
is_multiprocess_safe = False # Needs file locking; OS-specific.
is_thread_safe = False # Needs file locking or synchronization.
For Python3 we now use the highest protocol at time of writing, being protocol 4 (it was 2)
pickle_protocol = 4
__init__
takes a directory name, with an option to create it if
it’s not already there.
def __init__(self, directory, create=False):
directory = os.path.abspath(directory)
Make sure the directory exists:
if not os.path.exists(directory):
if create:
os.mkdir(directory)
else:
raise OSError("error, '%s' does not exist." % (directory,))
Is it actually a directory?
if not os.path.isdir(directory):
raise OSError("error, '%s' is not a directory." % (directory,))
self.directory = directory
Build the filename from the session ID.
def _make_filename(self, id):
return os.path.join(self.directory, id)
Pickle the session and save it into a file.
def save_session(self, session):
filename = self._make_filename(session.id)
f = open(filename, 'wb')
We wait at the following statement until we get an exclusive lock.
Note that load_session()
can sometimes jump in here before we get the lock
(the naughty thing) but it will get a zero-sized file (wb
mode truncates the file)
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
dump(session, f, self.pickle_protocol)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
f.close()
Load the pickled session from a file.
def load_session(self, id, default=None):
filename = self._make_filename(id)
finished = False
while not finished:
try:
f = open(filename, 'rb')
Sometimes we get the following lock AFTER save_session()
has created
the file but BEFORE it has locked it. If so, we’ll have a zero-sized file
(hence the loop, BTW, so don’t be tempted to remove it).
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
if os.stat(f.fileno()).st_size == 0:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
f.close()
Wait around for a bit and then loop…
time.sleep(SLEEPY_TIME)
else:
try:
obj = load(f)
Don’t be tempted to move this into a finally
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
f.close()
finished = True
except EOFError:
Sometimes we’ll also get EOFError
from pickle anyway, so we might
as well trap for that too (and then loop)…
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
f.close()
time.sleep(SLEEPY_TIME)
except OSError:
obj = default
finished = True
return obj
Delete the session file.
def delete_session(self, session):
filename = self._make_filename(session.id)
os.unlink(filename)
Delete all sessions that have not been modified for N minutes.
This method is never called by the session manager. It’s for your application maintenance program; e.g., a daily cron job.
DirectorySessionStore.delete_old_sessions returns a tuple:
(n_deleted, n_remaining)
def delete_old_sessions(self, minutes) -> tuple[int, int]:
deleted = 0
remaining = 0
for sess_id in os.listdir(self.directory):
pth = self._make_filename(sess_id)
mtime = os.stat(pth).st_mtime
inactive_for = (time.time() - mtime) / 60.0
if inactive_for > minutes:
os.unlink(pth)
deleted += 1
else:
remaining += 1
return deleted, remaining