ScoDoc/app/scodoc/sco_cache.py

271 lines
8.7 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2021-01-01 17:51:08 +01:00
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
2021-07-15 15:05:54 +02:00
"""Gestion des caches
2020-09-26 16:19:37 +02:00
2021-07-27 14:33:11 +02:00
-écrite pour ScoDoc8, utilise flask_caching et REDIS
2021-07-15 15:05:54 +02:00
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache en mémoire partagé.
2020-09-26 16:19:37 +02:00
"""
2021-07-15 15:05:54 +02:00
# API ScoDoc8 pour les caches:
2021-07-19 19:53:01 +02:00
# sco_cache.NotesTableCache.get( formsemestre_id)
2021-07-15 15:05:54 +02:00
# => sco_cache.NotesTableCache.get(formsemestre_id)
#
# sco_core.inval_cache(context, formsemestre_id=None, pdfonly=False, formsemestre_id_list=None)
2021-07-19 19:53:01 +02:00
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False)
2021-07-15 15:05:54 +02:00
#
#
# Nouvelles fonctions:
# sco_cache.NotesTableCache.delete(formsemestre_id)
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
#
# Bulletins PDF:
# sco_cache.PDFBulCache.get(formsemestre_id, version)
# sco_cache.PDFBulCache.set(formsemestre_id, version, filename, pdfdoc)
# sco_cache.PDFBulCache.delete(formsemestre_id) suppr. toutes les versions
# Evaluations:
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
#
2021-07-19 19:53:01 +02:00
import time
2021-07-26 15:18:16 +02:00
import traceback
2021-07-15 15:05:54 +02:00
from flask import g
2021-07-19 19:53:01 +02:00
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
2021-07-15 15:05:54 +02:00
from app.scodoc.notes_log import log
2021-07-19 19:53:01 +02:00
CACHE = None # set in app.__init__.py
2021-07-15 15:05:54 +02:00
class ScoDocCache:
"""Cache for ScoDoc objects.
keys are prefixed by the current departement.
"""
2021-07-19 19:53:01 +02:00
timeout = None # ttl, infinite by default
2021-07-15 15:05:54 +02:00
prefix = ""
@classmethod
def _get_key(cls, oid):
return g.scodoc_dept + "_" + cls.prefix + "_" + oid
2021-07-15 15:05:54 +02:00
@classmethod
def get(cls, oid):
"""Returns cached evaluation, or None"""
2021-07-26 15:18:16 +02:00
try:
return CACHE.get(cls._get_key(oid))
2021-07-27 14:33:11 +02:00
except:
log("XXX CACHE Warning: error in get")
2021-07-26 15:18:16 +02:00
log(traceback.format_exc())
return None
2021-07-15 15:05:54 +02:00
@classmethod
def set(cls, oid, value):
"""Store value"""
key = cls._get_key(oid)
2021-07-26 09:50:22 +02:00
# log(f"CACHE key={key}, type={type(value)}, timeout={cls.timeout}")
2021-07-26 17:11:45 +02:00
try:
status = CACHE.set(key, value, timeout=cls.timeout)
if not status:
log("Error: cache set failed !")
2021-07-27 14:33:11 +02:00
except:
log("XXX CACHE Warning: error in set !!!")
2021-07-26 17:11:45 +02:00
return status
2021-07-20 06:52:42 +02:00
2021-07-15 15:05:54 +02:00
@classmethod
def delete(cls, oid):
"""Remove from cache"""
CACHE.delete(cls._get_key(oid))
@classmethod
def delete_many(cls, oids):
"""Remove multiple keys at once"""
# delete_many seems bugged:
# CACHE.delete_many([cls._get_key(oid) for oid in oids])
for oid in oids:
cls.delete(oid)
2021-07-15 15:05:54 +02:00
class EvaluationCache(ScoDocCache):
2021-07-21 14:58:49 +02:00
"""Cache for evaluations.
Clé: evaluation_id
Valeur: { 'etudid' : note }
"""
2021-07-15 15:05:54 +02:00
prefix = "EVAL"
@classmethod
2021-07-19 19:53:01 +02:00
def invalidate_sem(cls, formsemestre_id):
"delete evaluations in this formsemestre from cache"
req = """SELECT e.evaluation_id
FROM notes_formsemestre s, notes_evaluation e, notes_moduleimpl m
WHERE s.formsemestre_id = %(formsemestre_id)s and s.formsemestre_id=m.formsemestre_id and e.moduleimpl_id=m.moduleimpl_id;
2021-07-15 15:05:54 +02:00
"""
2021-07-19 19:53:01 +02:00
evaluation_ids = [
x[0]
for x in ndb.SimpleQuery(None, req, {"formsemestre_id": formsemestre_id})
]
cls.delete_many(evaluation_ids)
2021-07-15 15:05:54 +02:00
@classmethod
2021-07-19 19:53:01 +02:00
def invalidate_all_sems(cls):
"delete all evaluations from cache"
evaluation_ids = [
x[0]
for x in ndb.SimpleQuery(
None, "SELECT evaluation_id FROM notes_evaluation", ""
)
]
cls.delete_many(evaluation_ids)
class AbsSemEtudCache(ScoDocCache):
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
Ce cache étant indépendant des semestre, le compte peut être faux lorsqu'on
change les dates début/fin d'un semestre.
C'est pourquoi il expire après timeout secondes.
Le timeout evite aussi d'éliminer explicitement ces éléments cachés lors
des suppressions d'étudiants ou de semestres.
2021-07-20 06:52:42 +02:00
Clé: etudid + "_" + date_debut + "_" + date_fin
Valeur: (nb_abs, nb_abs_just)
2021-07-19 19:53:01 +02:00
"""
2021-07-15 15:05:54 +02:00
2021-07-20 06:52:42 +02:00
prefix = "ABSE"
2021-07-19 19:53:01 +02:00
timeout = 60 * 60 # ttl 60 minutes
2021-07-15 15:05:54 +02:00
2021-07-19 19:53:01 +02:00
class SemBulletinsPDFCache(ScoDocCache):
"""Cache pour les classeurs de bulletins PDF d'un semestre.
Document pdf assez volumineux. La clé inclut le type de bulletin (version).
Clé: formsemestre_id_version
Valeur: (filename, pdfdoc)
"""
2021-07-15 15:05:54 +02:00
2021-07-19 19:53:01 +02:00
prefix = "SBPDF"
timeout = 12 * 60 * 60 # ttl 12h
2020-09-26 16:19:37 +02:00
2021-07-19 19:53:01 +02:00
@classmethod
def invalidate_sems(cls, formsemestre_ids):
"""Clear cached pdf for all given formsemestres"""
for version in scu.BULLETINS_VERSIONS:
oids = [
formsemestre_id + "_" + version for formsemestre_id in formsemestre_ids
]
cls.delete_many(oids)
class SemInscriptionsCache(ScoDocCache):
"""Cache les inscriptions à un semestre.
Clé: formsemestre_id
Valeur: liste d'inscriptions
[ {'formsemestre_inscription_id': 'SI78677', 'etudid': '1234', 'formsemestre_id': 'SEM012', 'etat': 'I', 'etape': ''}, ... ]
"""
2020-09-26 16:19:37 +02:00
2021-07-19 19:53:01 +02:00
prefix = "SI"
duration = 12 * 60 * 60 # ttl 12h
2020-09-26 16:19:37 +02:00
2021-07-19 19:53:01 +02:00
class NotesTableCache(ScoDocCache):
"""Cache pour les NotesTable
Clé: formsemestre_id
Valeur: NotesTable instance
"""
2020-09-26 16:19:37 +02:00
2021-07-19 19:53:01 +02:00
prefix = "NT"
2020-09-26 16:19:37 +02:00
2021-07-19 19:53:01 +02:00
@classmethod
def get(cls, formsemestre_id, compute=True):
2021-07-19 19:53:01 +02:00
"""Returns NotesTable for this formsemestre
2021-07-27 14:33:11 +02:00
Search in local cache (g.nt_cache) or global app cache (eg REDIS)
If not in cache and compute is True, build it and cache it.
2021-07-19 19:53:01 +02:00
"""
2021-07-27 14:33:11 +02:00
# try local cache (same request)
if not hasattr(g, "nt_cache"):
g.nt_cache = {}
else:
if formsemestre_id in g.nt_cache:
return g.nt_cache[formsemestre_id]
# try REDIS
2021-07-19 19:53:01 +02:00
key = cls._get_key(formsemestre_id)
nt = CACHE.get(key)
if nt or not compute:
2021-07-19 19:53:01 +02:00
return nt
2021-07-27 14:33:11 +02:00
# Recompute asked table:
2021-07-19 19:53:01 +02:00
from app.scodoc import notes_table
t0 = time.time()
nt = notes_table.NotesTable(None, formsemestre_id)
dt = time.time() - t0
log("caching formsemestre_id=%s (%gs)" % (formsemestre_id, dt))
_ = cls.set(formsemestre_id, nt)
2021-07-27 14:33:11 +02:00
g.nt_cache[formsemestre_id] = nt
2021-07-19 19:53:01 +02:00
return nt
def invalidate_formsemestre( # was inval_cache( context, formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False
):
"""expire cache pour un semestre (ou tous si formsemestre_id non spécifié).
Si pdfonly, n'expire que les bulletins pdf cachés.
2020-09-26 16:19:37 +02:00
"""
2021-07-19 19:53:01 +02:00
from app.scodoc import sco_parcours_dut
log("inval_cache, formsemestre_id=%s pdfonly=%s" % (formsemestre_id, pdfonly))
if formsemestre_id is None:
# clear all caches
log("----- invalidate_formsemestre: clearing all caches -----")
formsemestre_ids = [
x[0]
for x in ndb.SimpleQuery(
None, "SELECT formsemestre_id FROM notes_formsemestre", ""
)
]
else:
formsemestre_ids = [
formsemestre_id
] + sco_parcours_dut.list_formsemestre_utilisateurs_uecap(None, formsemestre_id)
log(f"----- invalidate_formsemestre: clearing {formsemestre_ids} -----")
if not pdfonly:
# Delete cached notes and evaluations
NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id:
for formsemestre_id in formsemestre_ids:
EvaluationCache.invalidate_sem(formsemestre_id)
2020-09-26 16:19:37 +02:00
else:
2021-07-19 19:53:01 +02:00
# optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems()
SemInscriptionsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)