Repository: GeotrekCE/Geotrek-admin

Issue:
Bad performances when creating a path that intersects a long trek



Discussion:
Le problème a été identifié sur la nouvelle version 2.33. Mais en testant, le problème était déjà présent dans les versions précédentes. Problème similaire en testant sur une version 2.32.5. 
Quand je créé un tronçon coupant un tronçon existant qui n'est utilisé par aucun événement, alors pas de soucis. Quand je créé un tronçon qui coupe un tronçon utilisé par des événements, alors c'est très long, voire ça n'aboutit pas et finit en Timeout. Je n'y vois pas forcément clair si c'est le cas uniquement quand les événements rattachés au tronçon intersecté sont nombreux et longs ou pas. 
A creuser/investiguer.
Même problème sur la version 2.33.7, en voulant raccorder un nouveau tronçon à un tronçon existant qui contient plusieurs événements (Itinéraires parents, étapes, POIs, Contenus Touristiques). 

Après un certains temps de chargement le navigateur affiche une erreur 500 (avec un timeout mis à 7200 au lieu de 300).

La commande htop exécutée sur le serveur lors de l'opération montre que la ram est peu sollicitée, idem pour les processeurs (1 sur 4 à 100%, les autres proche du 0).

Le log geotrek-ui affiche le message suivant :
Jun 06 15:20:59 vps650123 gunicorn[6857]:     num = len(clone)
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/models/query.py", line 256, in __len__
Jun 06 15:20:59 vps650123 gunicorn[6857]:     self._fetch_all()
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/models/query.py", line 1242, in _fetch_all
Jun 06 15:20:59 vps650123 gunicorn[6857]:     self._result_cache = list(self._iterable_class(self))
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/models/query.py", line 55, in __iter__
Jun 06 15:20:59 vps650123 gunicorn[6857]:     results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/models/sql/compiler.py", line 1138, in execute_sql
Jun 06 15:20:59 vps650123 gunicorn[6857]:     cursor = self.connection.cursor()
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/backends/base/base.py", line 256, in cursor
Jun 06 15:20:59 vps650123 gunicorn[6857]:     return self._cursor()
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/backends/base/base.py", line 233, in _cursor
Jun 06 15:20:59 vps650123 gunicorn[6857]:     self.ensure_connection()
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/backends/base/base.py", line 217, in ensure_connec
Jun 06 15:20:59 vps650123 gunicorn[6857]:     self.connect()
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/utils.py", line 89, in __exit__
Jun 06 15:20:59 vps650123 gunicorn[6857]:     raise dj_exc_value.with_traceback(traceback) from exc_value
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/backends/base/base.py", line 217, in ensure_connec
Jun 06 15:20:59 vps650123 gunicorn[6857]:     self.connect()
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/backends/base/base.py", line 195, in connect
Jun 06 15:20:59 vps650123 gunicorn[6857]:     self.connection = self.get_new_connection(conn_params)
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/django/db/backends/postgresql/base.py", line 178, in get_new
Jun 06 15:20:59 vps650123 gunicorn[6857]:     connection = Database.connect(**conn_params)
Jun 06 15:20:59 vps650123 gunicorn[6857]:   File "/opt/geotrek-admin/lib/python3.6/site-packages/psycopg2/__init__.py", line 127, in connect
Jun 06 15:20:59 vps650123 gunicorn[6857]:     conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
Jun 06 15:20:59 vps650123 gunicorn[6857]: django.db.utils.OperationalError: FATAL:  the database system is in recovery mode
Jun 06 15:20:59 vps650123 gunicorn[6857]: FATAL:  the database system is in recovery mode

Comment puis je solutionner le fait d'avoir la databse en recovery mode ? A savoir qu'il m'est toujours possible de créer de nouveaux objets.

Merci,

Jean
D'après ce que je lis le recovery mode se déclenche entre autres si un des processus de postgresql crashe et il se termine tout seul quand le recovery est fini. Il faudrait regarder les logs de postgresql pour en savoir plus. Une cause possible de crash de postgresql peut être le OOM killer. Un coup d'oeil au log système permettrait de valider ou pas cette hypothèse.
Merci @gutard pour ton retour.

Je ne suis pas expert en la matière, mais je n'ai pas su trouver si un process avait été arrêté par le OOM Killer. Je n'ai pas de kern.log lié à la période, ni d'info en exécutant $dmesg | less dans la console.

Néanmoins, voici les logs qui correspondent à la période durant laquelle l'ajout de tronçon a été effectué. Je peux toujours rajouter un tronçon, mais lorsque je veux le raccorder à un tronçon existant le problème se pose à nouveau.

[geotrek_error.log](https://github.com/GeotrekCE/Geotrek-admin/files/4750783/geotrek_error.log)
[geotrek-ui.log](https://github.com/GeotrekCE/Geotrek-admin/files/4750785/geotrek-ui.log)
[postgresql.log](https://github.com/GeotrekCE/Geotrek-admin/files/4750786/postgresql.log)
[sys.log](https://github.com/GeotrekCE/Geotrek-admin/files/4750787/sys.log)

- Topologies inutiles qui traînent dans la base (kind=TMP ou TOPOLOGY) ?
- Nécessité de draper les edges ?
- Nécessité de draper les topologies deleted=TRUE ?

Dans le cas identifié initialement sur le test sur les données PNE, ça tombait en timeout en enregistrant un nouveau tronçon coupant un tronçon utilisé par de longs événements (à priori). Je ne sais pas si le problème de @JeanLenormand est la même chose avec sa BDD qui tombe en recovery mode.
Le problème est que quand le serveur timeoute sur la création ou l'édition d'une topologie, il laisse une topologie temporaire (kind='TMP'). Ces topologies sont recalculées quand on édite un de leurs tronçons et si il y a beaucoup de topologies temporaires (plusieurs dizaines en l'occurrence) ça prend beaucoup de temps.

Fix rapide (à appliquer avec précaution, après backup, je décline toute responsabilité en cas de perte de données) pour ceux qui le souhaitent :
```
DELETE FROM core_pathaggregation USING core_topology \
       WHERE core_topology.id=topo_object_id AND kind IN ('TMP', 'TOPOLOGY');

DELETE FROM core_topology \
       WHERE kind IN ('TMP', 'TOPOLOGY');
```

Cependant, tout nouveau timeout à l'édition d'un tronçon recréera des nouvelles topologies temporaire, même si c'est beaucoup moins probable depuis la version 2.33 qui améliore les performances.

Fix définitif prévu :
* Faire le nettoyage dans une migration ?
* Encadrer la création/édition d'une topologie dans une transaction SQL

Piste pour aller plus loin :
* Ne pas créer de topologies temporaires en base ?
* Ne pas drapper les CityEdge/DistrictEdge ?

OK merci pour ces éléments et débuts de résolution.
Du coup je comprends la suppression des évènements de type TMP mais pourquoi aussi ceux de type TOPOLOGY ?

En effet encapsuler les insertion et modification dans une transaction annulée globalement en cas de timeout ou de plantage me semble le mieux.
> pourquoi aussi ceux de type TOPOLOGY

TOPOLOGY est devenu TMP quand on a amélioré les perfs sur l'édition des topologies. Les événements qui ont été créés avant la 2.33 sont de type TOPOLOGY et après de type TMP. Les topologies non temporaires sont de type TREK, POI, etc.
OK, merci pour les précisions.
Le soucis des topologies temporaires est corrigé dans la version 2.48.0, et ainsi il n'en sera plus créées de nouvelles.
Est-il par contre conseillé de supprimé manuellement celles qui se seraient accumulées dans la BDD auparavant ?
Ou c'est géré par les migrations automatiquement ?
Si il faut le faire manuellement, le SQL indiqué plus haut est celui à appliquer ?

Merci.

Resolving patch:
diff --git a/geotrek/maintenance/forms.py b/geotrek/maintenance/forms.py
--- a/geotrek/maintenance/forms.py
+++ b/geotrek/maintenance/forms.py
@@ -119,7 +119,7 @@ def __init__(self, *args, target_type=None, target_id=None, **kwargs):
                 self.helper.form_action += '?target_type={}&target_id={}'.format(target_type, target_id)
             else:
                 # Point target to a new topology
-                self.instance.target = Topology()
+                self.instance.target = Topology(kind='INTERVENTION')
         # Else: existing intervention. Target is already set
 
         self.fields['topology'].initial = self.instance.target
diff --git a/geotrek/maintenance/migrations/0017_auto_20210211_0825.py b/geotrek/maintenance/migrations/0017_auto_20210211_0825.py
new file mode 100644
--- /dev/null
+++ b/geotrek/maintenance/migrations/0017_auto_20210211_0825.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.1.5 on 2021-02-11 08:25
+
+from django.db import migrations
+
+
+def fix_topology_kind(apps, schema_editor):
+    # Fix topology kind of interventions
+    Topology = apps.get_model('core', 'topology')
+    ContentType = apps.get_model('contenttypes', 'contenttype')
+    Intervention = apps.get_model('maintenance', 'intervention')
+    topology_ct = ContentType.objects.get_for_model(Topology)
+    topo_ids = Intervention.objects.filter(target_type=topology_ct).values_list('target_id', flat=True)
+    topos = Topology.objects.filter(id__in=topo_ids).filter(kind__in=('TMP', 'TOPOLOGY'))
+    n = topos.update(kind='INTERVENTION')
+    if n:
+        print("\n  WARNING! Updated {} topologies directly linked to interventions with a wrong kind".format(n))
+
+
+def clean_tmp_topologies(apps, schema_editor):
+    # Remove temporary topologies
+    Topology = apps.get_model('core', 'topology')
+    Topology.objects.filter(kind__in=('TMP', 'TOPOLOGY')).delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('maintenance', '0016_auto_20210121_0943'),
+    ]
+
+    operations = [
+        migrations.RunPython(fix_topology_kind),
+        migrations.RunPython(clean_tmp_topologies),
+    ]


Test patch:
diff --git a/geotrek/maintenance/tests/test_views.py b/geotrek/maintenance/tests/test_views.py
--- a/geotrek/maintenance/tests/test_views.py
+++ b/geotrek/maintenance/tests/test_views.py
@@ -412,6 +412,7 @@ def test_creation_form_line(self):
         self.assertEqual(PathAggregation.objects.count(), 1)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Intervention.objects.first().geom, path.geom)
+        self.assertEqual(Intervention.objects.first().target.kind, 'INTERVENTION')
 
 
 class ProjectViewsTest(CommonTest):