Aller au contenu

Les sessions persistantes dans Forge (forge-mvc-sessions-db)

forge-mvc-sessions-db fournit DbSessionStore, un store de session adossé à la base de données (table forge_sessions).

Le cœur de Forge, agnostique du SGBD, ne fournit qu'un store mémoire et un store fichier ; ce paquet ajoute le store BDD, partagé entre processus et persistant.

1. Rôle du module

Une session Forge conserve l'état d'un visiteur entre deux requêtes (jeton CSRF, utilisateur authentifié, messages flash).

Le store par défaut du cœur (MemorySessionStore) garde ces données en mémoire du processus : elles disparaissent au redémarrage et ne sont pas partagées entre workers.

DbSessionStore stocke chaque session dans la table forge_sessions de la base configurée du projet, ce qui la rend partagée entre processus et durable.

2. Installation

Prérequis : activez le venv du projet

Quelle que soit la source, installez dans le venv du projet :

source .venv/bin/activate

Lancé hors d'un venv, pip vise le Python système (Debian 12+, Ubuntu 23.04+),
protégé par PEP 668. Il refuse alors d'installer, pour ne pas écraser les paquets
gérés par apt, et affiche externally-managed-environment.
Le venv de projet créé par forge new n'a pas ce verrou.

Installer le paquet

A. Depuis PyPI (stable)

La dernière version publiée :

pip install --pre forge-mvc-sessions-db

B. Depuis Git (avant-garde)

Cœur puis opt-in depuis git, dans le venv du projet (l'opt-in trouve le cœur git déjà en place, sans version publiée sur PyPI) :

pip install "git+https://github.com/caucrogeGit/Forge.git@main"
pip install "git+https://github.com/caucrogeGit/Forge.git@main#subdirectory=packages/forge-mvc-sessions-db"

Cet opt-in est une bibliothèque : on l'importe et on passe le store à forge.configure, il n'y a pas de câblage de routes.

import core.forge as forge
from forge_mvc_sessions_db import DbSessionStore

forge.configure(session_store=DbSessionStore(ttl=3600))
3. Mise en service

Installer le paquet ne suffit pas à le rendre opérationnel.
Voici les gestes propres à forge-mvc-sessions-db, dans l'ordre.

Ils déclinent la procédure canonique, Rendre un opt-in opérationnel : les cinq
points
.

1. L'épingler

forge-mvc-sessions-db==<version de forge-mvc>

Dans requirements.txt, à la même version ou au même commit que forge-mvc.
Sans cette ligne, l'opt-in n'existe que sur votre machine.

2. L'inscrire

forge opt-in:enable sessions-db --apply

L'opt-in est inscrit dans optins/registry.py (ADR-061), ce qui le rend visible du
projet.
--apply est obligatoire : sans lui, la commande simule et n'écrit rien.

3. Poser ce dont il a besoin

forge sessions:init
forge migration:apply

sessions:init copie la migration embarquée dans mvc/migrations/ ;
migration:apply l'exécute et la trace (ADR-071).
Sans cette étape, le premier appel échoue sur une table absente.

4. Le brancher là où il agit

Il s'importe dans le code qui s'en sert. Il n'y a ni route à monter ni middleware
à poser.

5. Le prouver

make check
forge doctor

Puis un premier usage réel.
Un opt-in installé, inscrit et provisionné qu'aucun code n'appelle n'est pas
opérationnel : il est seulement présent.

4. Désinstallation
pip uninstall forge-mvc-sessions-db

Le cœur revient alors à son store par défaut (MemorySessionStore).
forge opt-in:remove sessions-db affiche la commande pip uninstall sans l'exécuter.

5. Commandes

Cet opt-in n'expose aucune commande CLI : il s'utilise par import dans le code applicatif (voir l'API publique ci-dessous).

6. Vue d'ensemble rapide
Élément Valeur
Paquet forge-mvc-sessions-db
Module forge_mvc_sessions_db
Catégorie Exploitation et outillage (ADR-055)
Couche opt-in (brique optionnelle)
Dépend de forge-mvc et un backend BDD (ADR-054)
API publique DbSessionStore
Table SQL forge_sessions
Exécuteurs injectés en callables (fetch_one, execute), défaut core.database.db
Contrat implémenté core.sessions.SessionStore
Principe SQL portable, horodatages calculés côté Python (pas de NOW() propriétaire)
Décision d'architecture ADR-054 (backends BDD et extraction du store de session)
Installation pip install --pre forge-mvc-sessions-db
7. Schémas UML

Le diagramme de classe montre l'implémentation du contrat ; le diagramme de séquence montre le cycle d'une session persistée.

5.1 Diagramme de classe

DbSessionStore implémente l'intégralité du contrat SessionStore du cœur et délègue tout son SQL à un exécuteur injecté.

Il ajoute cleanup_expired(), qui ne figure pas au contrat : purger les sessions périmées n'a de sens que pour un store persistant, un store en mémoire disparaissant avec le processus.

classDiagram
    class SessionStore {
        <<protocol>>
        +create(data) str
        +get(session_id) dict
        +set(session_id, data) None
        +replace(session_id, data) None
        +delete(session_id) None
        +regenerate(session_id) str
        +authenticate(session_id, user_data, ttl) str
        +touch_expiry(session_id, ttl) bool
    }
    class DbSessionStore {
        -_fetch_one
        -_execute
        -_ttl
        +create(data) str
        +get(session_id) dict
        +set(session_id, data) None
        +replace(session_id, data) None
        +delete(session_id) None
        +regenerate(session_id) str
        +authenticate(session_id, user_data, ttl) str
        +touch_expiry(session_id, ttl) bool
        +cleanup_expired() int
    }
    class forge_sessions {
        <<table>>
        session_id
        data
        expire_at
        created_at
        updated_at
    }
    SessionStore <|.. DbSessionStore : implémente
    DbSessionStore ..> forge_sessions : lit / écrit via core.database.db

Ce que le diagramme révèle :

  • DbSessionStore respecte le contrat SessionStore, donc il se configure comme n'importe quel autre store ;
  • les données vivent dans la table forge_sessions ;
  • le store ne touche jamais la base directement : il passe par les exécuteurs fetch_one / execute (par défaut ceux de core.database.db).

5.2 Diagramme de séquence

sequenceDiagram
    participant App as Application
    participant Store as DbSessionStore
    participant DB as core.database.db

    App->>Store: create()
    Store->>DB: INSERT forge_sessions (id, data, expire_at, created_at, updated_at)
    App->>Store: get(session_id)
    Store->>DB: SELECT data WHERE id = ? AND expire_at > ?
    DB-->>Store: data JSON
    Store-->>App: dict de session
    App->>Store: cleanup_expired()
    Store->>DB: DELETE WHERE expire_at < ?

L'horodatage comparé (?) est calculé côté Python, jamais par une fonction SQL propriétaire.

8. API publique
Nom Signature Rôle
DbSessionStore DbSessionStore(fetch_one=None, execute=None, ttl=SESSION_TTL) Store de session BDD ; exécuteurs injectables, durée de vie en secondes
create create(data=None) -> str Crée une session (structure Forge standard) et retourne son identifiant
get get(session_id) -> dict | None Retourne les données, ou None si absente, expirée ou corrompue
set set(session_id, data) -> None Met à jour (merge) une session existante
replace replace(session_id, data) -> None Remplace intégralement les données (sans merge)
delete delete(session_id) -> None Supprime la session
regenerate regenerate(session_id) -> str Nouvel identifiant, données préservées (anti-fixation)
authenticate authenticate(session_id, user_data, ttl_seconds) -> str | None Rotation atomique vers une session authentifiée
touch_expiry touch_expiry(session_id, ttl_seconds) -> bool Repousse l'expiration
cleanup_expired cleanup_expired() -> int Supprime les sessions expirées, retourne le nombre supprimé

Le module expose aussi set_flash / get_flash pour les messages flash.

9. Contextes d'utilisation
Besoin Store recommandé
Développement local, tests MemorySessionStore (cœur)
Persistance mono-processus simple FileSessionStore (cœur)
Production multi-worker (Gunicorn, uWSGI) DbSessionStore (cet opt-in)
Déploiement multi-nœud derrière la même base DbSessionStore (cet opt-in)
10. Exemples d'utilisation

8.1 Configurer le store

import core.forge as forge
from forge_mvc_sessions_db import DbSessionStore

forge.configure(session_store=DbSessionStore(ttl=3600))

La table forge_sessions doit exister au préalable (forge sessions:init puis forge migration:apply).

8.2 Nettoyer les sessions expirées

from forge_mvc_sessions_db import DbSessionStore

store = DbSessionStore()
supprimees = store.cleanup_expired()
print(f"{supprimees} sessions expirées supprimées")

En ligne de commande, forge sessions:gc fait la même purge :

forge sessions:gc

Rien n'est planifié automatiquement : branchez forge sessions:gc sur un cron ou un systemd timer.

8.3 Tester sans base réelle

from forge_mvc_sessions_db import DbSessionStore

rows = {}

def fake_fetch_one(sql, params):
    row = rows.get(params[0])
    return {"data": row} if row else None

def fake_execute(sql, params=()):
    return 1

store = DbSessionStore(fetch_one=fake_fetch_one, execute=fake_execute)

Les exécuteurs fetch_one / execute sont injectables : les tests n'ont pas besoin d'une base.

11. Portabilité et exécuteurs injectés

Le store délègue tout son SQL aux callables fetch_one / execute, qui pointent par défaut vers core.database.db.

core.database.db dispatche vers le backend BDD actif (forge-mvc-mariadb, forge-mvc-sqlite, etc.), en traduisant le style de paramètres (?) et le dialecte.

Comme les horodatages sont calculés côté Python et passés en paramètres, aucune fonction date propriétaire n'apparaît dans le SQL : le store fonctionne à l'identique sur tous les backends (ADR-054).

Création de la table

Le store suppose la table forge_sessions présente.
Elle n'est pas créée automatiquement : lancez forge sessions:init puis forge migration:apply.

12. Une durée de vie par nature de session

Le store portait une durée pour tout le monde (SESSIONS-TTL-PER-KIND-001). Les trois natures de session n'ont pourtant ni le même risque ni le même usage.

Nature Ce qu'elle porte Ce qu'une fuite coûte
anonymous un jeton CSRF, un panier presque rien
authenticated une identité l'accès au compte
remembered une identité, sur des semaines l'accès au compte, longtemps

Une durée unique force un arbitrage perdant. Réglée court, elle déconnecte les utilisateurs authentifiés toutes les heures. Réglée long, elle laisse traîner des sessions anonymes par milliers, que la purge doit balayer et qui occupent la table pour un jeton CSRF.

SESSION_TTL_ANONYMOUS=7200
SESSION_TTL_AUTHENTICATED=3600
SESSION_TTL_REMEMBERED=2592000

Le vocabulaire est fermé

Une quatrième nature inventée par une application rendrait la métrique et la purge incomparables d'un projet à l'autre, ce qui est ce que ce champ doit permettre.

Une valeur illisible lève

Comme pour les quotas de forge-mvc-files et les limites de forge-mvc-images.

Retomber en silence sur le défaut donnerait une durée que personne n'a écrite, et une session qui expire trop tôt se diagnostique très mal.

Le réglage de l'authentifié ne servait à rien

ttl_for() n'était appelée qu'à un seul endroit, la création d'une session anonyme (SESSIONS-TTL-AUTHENTICATED-APPLIED-001).

La connexion passe par authenticate(), qui prenait le ttl_seconds de son appelant, et le cœur appelle avec SESSION_DURATION, égal au défaut historique.
Mesuré, un exploitant réglant SESSION_TTL_AUTHENTICATED=1800 obtenait trois mille six cents secondes quand même.

C'est un réglage de sécurité : celui qui l'a posé croyait ses sessions raccourcies, et elles ne l'étaient pas.
Le module refuse pourtant une valeur illisible, en disant que « retomber en silence sur le défaut donnerait une durée que personne n'a écrite » ; la valeur lisible était ignorée tout aussi silencieusement.

Les deux chemins suivent désormais la même règle, et un garde-fou lu sur l'arbre syntaxique refuse qu'ils divergent à nouveau.

La nature remembered s'écrit depuis l'application

Le protocole SessionStore du cœur déclare create(data=None), sans nature : Forge n'implémente pas de « se souvenir de moi ».

Une application qui en veut un tient son DbSessionStore et appelle create(kind=KIND_REMEMBERED) elle même.
La durée est prête, le geste lui appartient.

Un ttl passé au constructeur reste prioritaire

Un projet qui l'avait réglé à la main garde son réglage : le retirer sous ses pieds serait une rupture silencieuse.

La colonne kind arrive par une migration additive. Les projets déjà provisionnés ne rejouent pas la création de la table, son empreinte étant enregistrée.

13. Compter les sessions actives

sessions:gc dit combien de sessions il a purgées. Personne ne pouvait dire combien il en reste, ni comment ce nombre évolue (SESSIONS-ACTIVE-METRIC-001).

C'est pourtant la première chose qu'on veut savoir d'un magasin adossé à la base : une table qui grossit sans fin signale une purge qui ne tourne pas, et une chute brutale signale une déconnexion de masse.

from forge_mvc_sessions_db import session_metrics

mesures = session_metrics()
mesures.active, mesures.expired, mesures.by_kind
mesures.purge_backlog_ratio        # au delà de 0.5, la purge est en retard

Le filtre est en SQL

Compter toutes les lignes puis écarter les expirées en Python rapatrierait une table entière pour en rendre un nombre.

Une session expirée n'est pas active

Elle occupe la table sans plus servir à personne, même si la purge ne l'a pas encore retirée.

La compter ferait passer un retard de purge pour de la fréquentation, ce qui est le contraire de ce que la métrique doit montrer.

Les trois natures figurent toujours dans by_kind, à zéro le cas échéant : une clé absente et une valeur nulle se lisent différemment dans un tableau de bord, et l'absence ferait croire à une métrique cassée.

14. Faire tourner la purge, avec systemd

sessions:gc doit tourner régulièrement (SESSIONS-GC-TIMER-DOC-001). Forge ne fournit pas de planificateur : c'est le rôle du système, et en embarquer un ferait de Forge un ordonnanceur, ce que le principe 8 refuse.

Deux fichiers, dans /etc/systemd/system/.

# forge-sessions-gc.service
[Unit]
Description=Purge des sessions Forge expirées
After=network.target

[Service]
Type=oneshot
User=monapp
WorkingDirectory=/srv/monapp
EnvironmentFile=/srv/monapp/env/prod
ExecStart=/srv/monapp/.venv/bin/forge sessions:gc
# forge-sessions-gc.timer
[Unit]
Description=Purge horaire des sessions Forge

[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
sudo systemctl enable --now forge-sessions-gc.timer
systemctl list-timers forge-sessions-gc.timer
journalctl -u forge-sessions-gc.service --since today

Persistent=true rattrape les exécutions manquées

Un serveur éteint pendant la nuit ne saute pas trois purges : la première au redémarrage les remplace.

Sans lui, un serveur redémarré chaque matin ne purgerait jamais ce qui a expiré la nuit.

RandomizedDelaySec évite la synchronisation

Plusieurs applications purgeant à la même minute frappent la base ensemble.

Cinq minutes de dispersion suffisent à étaler la charge.

Le minuteur, pas le service

systemctl enable porte sur le .timer. Activer le .service le ferait tourner une fois au démarrage, puis plus jamais.

EnvironmentFile porte les identifiants de base

Le fichier doit appartenir au compte de service et n'être lisible que par lui, chmod 600.

sessions:gc se connecte à la base, et un env/prod lisible par tous rend les identifiants applicatifs lisibles par tous.

La fréquence dépend de la durée de vie la plus courte : purger toutes les heures des sessions anonymes de deux heures laisse la table à deux fois sa taille utile, ce qui est raisonnable. session_metrics().purge_backlog_ratio le vérifie.

Voir aussi