Development #58208
ne pas modifier les données en session dans autosave
0%
Description
Cf. #58161
En cas de forte charge on a visiblement des autosave et des soumissions classiques qui se croisent pour une même page, normalement ça doit bien se passer avec le autosave qui passe soit avant, soit qui est refusé parce que _ajax_form_token n'est plus valide, mais dans de rare cas on peut avoir un autosave qui se finit après la soumission de la page correspondante. Associé à #58207 ça peut amener à avoir des données normalement invalides dans formdata.data (champ requis à None).
Une solution serait d'attendre la fin d'un autosave en cours quand le bouton submit est cliqué, puis des les bloquer ensuite.
Fichiers
Révisions associées
Historique
Mis à jour par Frédéric Péters il y a plus de 2 ans
Peut-être partiel mais il y a déjà un bout qui tente de prévenir ça, dans test_form_autosave on peut lire :
# make sure autosave() doesn't destroy data that would have been submitted # in the meantime
Mis à jour par Frédéric Péters il y a plus de 2 ans
Le moment particulier étant j'imagine caché dans :
mais dans de rare cas
et ça serait :
# reload session to make sure _ajax_form_token is still valid session = get_session_manager().get(get_session().id) if not session: return result_error('cannot get ajax form token (lost session)') if not session.has_form_token(get_request().form.get('_ajax_form_token')): return result_error('obsolete ajax form token (late check)') # ici on pense avoir vérifié juste à temps, au plus proche, que le _ajax_form_token était ok # mais raté, pile maintenant il ne l'est plus. try: self.save_draft(form_data, page_no)
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch ajouté
- Fichier 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch ajouté
- Tracker changé de Bug à Development
- Statut changé de Nouveau à Solution proposée
- Patch proposed changé de Non à Oui
Ceinture et bretelle.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
Frédéric Péters a écrit :
Le moment particulier étant j'imagine caché dans :
mais dans de rare cas
et ça serait :
[...]
Oui, mais je pense vraiment que ça ne sert à rien d'essayer de faire plus compliqué coté serveur, on y arrivera pas, on écrit de toute façon en session sans verrou ni aucune protection contre les accès concurrents; c'est peine perdue. Par contre on peut se protéger efficacement coté front, en évitant de lancer plusieurs requêtes en même dans la même page (AJAX + </submit>), ça on peut l'empêcher facilement.
En même temps que j'écrivais tout ça j'ai pensé qu'on pourrait aussi éviter qu'autosave modifie les données en session, ce qui corrigerait il me semble les deux tickets (#58028 et #58027) en même temps :
diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 28449a96..8fe03c7a 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -1324,14 +1324,18 @@ class FormPage(Directory, FormTemplateMixin): session = get_session() if not session: return result_error('missing session') self.feed_current_data(magictoken) form_data = session.get_by_magictoken(magictoken, {}) + # prevent autosave to write into session concurrently with user's + # submits, only do it when initializing the draft formdata. + if form_data.get('draft_formdata_id'): + form_data = form_data.copy() if not form_data: return result_error('missing data') try: page = self.pages[page_no] except IndexError: # XXX: this should not happen but if pages use conditionals based
L'idée étant qu'une fois qu'on a établi le lien entre les données en session et le draft via la donnée spéciale "draft_formdata_id" on ne modifie plus les données en session dans autosave(), on travaille uniquement sur une copie. Normalement après la première page ou le premier autosave on sait qu'on aura plus de concurrence entre submit() et autosave, c'est toujours les données en session qui gagnent, on peut avoir un draft qui diffère de l'état actuel de la session, mais ça ne durera pas jusqu'à la soumission finale, puisqu'à chaque soumission on mettra à jour le draft avec les bonnes données validées.
PS: j'avais écrit tout ça avant de travailler sur la solution proposée plus haut mais j'ai oublié de poster.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch ajouté
- Fichier 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch ajouté
- Fichier 0003-js-prevent-autosave-while-user-is-active-58208.patch 0003-js-prevent-autosave-while-user-is-active-58208.patch ajouté
Et un bout de JS pour rendre moins fréquent l'autosave, surtout si l'utilisateur s'active.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch ajouté
- Fichier 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch ajouté
- Fichier 0003-js-prevent-autosave-while-user-is-active-58208.patch 0003-js-prevent-autosave-while-user-is-active-58208.patch ajouté
Éviter que l'autosave ne se relance lors du callback Ajax.complete, pendant la soumission où autosave_timeout_id est mis à null, si celui en cours à échouer.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch 0002-forms-prevent-autosave-from-overwriting-magictoken-s.patch ajouté
- Fichier 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch 0001-formdef-do-not-load-data-from-item-s-widgets-with-er.patch ajouté
- Fichier 0003-js-prevent-autosave-while-user-is-active-58208.patch 0003-js-prevent-autosave-while-user-is-active-58208.patch ajouté
Ne pas reposer le timeout d'autosave pendant qu'un autre est en cours même si on bouge la souris.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0001-forms-prevent-autosave-from-overwriting-magictoken-s.patch 0001-forms-prevent-autosave-from-overwriting-magictoken-s.patch ajouté
Partie JS retirée pour aller dans #58276.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch ajouté
Ça n'est clairement pas suffisant, même si les données ne sont pas modifiées elles sont anciennes et viendront écraser les données plus récentes puisque la session est systématiquement écrasée à chaque requête réussie et je remarque maintenant le traitement particulier fait quand la requête autosave échoue via get_request().ignore_session = True
.
J'adopte donc le même comportement vis à vis de la session si draft_formdata_id n'est pas modifié.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch ajouté
Et un test, qui ne passe pas sans le patch.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Sujet changé de formulaire: un autosave peut se terminer après la soumission à ne pas modifier les données en session dans autosave
Mis à jour par Frédéric Péters il y a plus de 2 ans
Pourquoi ne pas juste systématiquement poser "request.ignore_session = True" et enregistrer la session de manière explicite au seul moment où elle change, cette partie qui serait de l'ordre de :
@@ -1360,6 +1360,7 @@ class FormPage(Directory, FormTemplateMixin): def save_draft(self, data, page_no=None): filled = self.get_current_draft() or self.formdef.data_class()() + new_draft = bool(filled.id is None) if filled.id and filled.status != 'draft': raise SubmittedDraftException() filled.data = data @@ -1380,7 +1381,10 @@ class FormPage(Directory, FormTemplateMixin): if not filled.user_id: get_session().mark_anonymous_formdata(filled) - data['draft_formdata_id'] = filled.id + if new_draft: + data['draft_formdata_id'] = filled.id + get_session().store() +
?
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
Mis à jour par Frédéric Péters il y a plus de 2 ans
Il y a un autre moment à gérer, quand il est fait get_session().mark_anonymous_formdata(filled)
il faut enregistrer la session. (j'ai poussé une nouvelle branche qui ajoute ça).
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Statut changé de Solution proposée à En cours
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch ajouté
- Statut changé de En cours à Solution proposée
Ok.
Mis à jour par Benjamin Dauvergne il y a plus de 2 ans
- Fichier 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch 0001-forms-prevent-autosave-from-overwriting-session-s-da.patch ajouté
Rebasé.
Mis à jour par Frédéric Péters il y a plus de 2 ans
- Statut changé de Solution proposée à Résolu (à déployer)
Validé/poussé,
commit a616254174960353e3da8afb3ae6f9d50edd0253 Author: Frédéric Péters <fpeters@entrouvert.com> Date: Fri Nov 5 16:31:20 2021 +0100 forms: prevent autosave from overwriting session's data (#58208)
Mis à jour par Frédéric Péters il y a plus de 2 ans
- Statut changé de Résolu (à déployer) à Solution déployée
forms: prevent autosave from overwriting session's data (#58208)