From 5f950cb71ac47c6c41df1565c0732a34d930a73f Mon Sep 17 00:00:00 2001
From: Cyrille Bagard <nocbos@gmail.com>
Date: Fri, 24 Aug 2018 01:43:36 +0200
Subject: Fixed a rare dead lock when loading several files at the same time.

---
 src/analysis/loading.c |   1 +
 src/analysis/project.c | 103 ++++++++++++++++++++++++++++++++++++-------------
 2 files changed, 77 insertions(+), 27 deletions(-)

diff --git a/src/analysis/loading.c b/src/analysis/loading.c
index fa2118f..b8fbeea 100644
--- a/src/analysis/loading.c
+++ b/src/analysis/loading.c
@@ -765,6 +765,7 @@ void g_content_explorer_delete_group(GContentExplorer *explorer, wgroup_id_t wid
     g_mutex_lock(&explorer->mutex);
 
     group = g_content_explorer_find_group(explorer, wid);
+    assert(group != NULL);
 
     /* Supression des contenus chargés */
 
diff --git a/src/analysis/project.c b/src/analysis/project.c
index 83b41c4..66faa16 100644
--- a/src/analysis/project.c
+++ b/src/analysis/project.c
@@ -853,33 +853,38 @@ static void g_loading_handler_dispose(GLoadingHandler *handler)
     GContentResolver *resolver;             /* Resolveur de contenus       */
     size_t i;                               /* Boucle de parcours          */
 
-    /* Supression des groupes de travail */
+    /**
+     * On se sert du projet comme sentinelle pour la validité des
+     * identifiants handler->exp_wids[i].
+     */
 
-    explorer = get_current_content_explorer();
-    resolver = get_current_content_resolver();
+    if (handler->project != NULL)
+    {
+        /* Supression des groupes de travail */
 
-    g_signal_handlers_disconnect_by_func(explorer, G_CALLBACK(on_new_content_explored), handler);
-    g_signal_handlers_disconnect_by_func(resolver, G_CALLBACK(on_new_content_resolved), handler);
+        explorer = get_current_content_explorer();
+        resolver = get_current_content_resolver();
 
-    g_mutex_lock(&handler->mutex);
+        g_signal_handlers_disconnect_by_func(explorer, G_CALLBACK(on_new_content_explored), handler);
+        g_signal_handlers_disconnect_by_func(resolver, G_CALLBACK(on_new_content_resolved), handler);
 
-    for (i = 0; i < handler->exp_count; i++)
-    {
-        g_content_resolver_delete_group(resolver, handler->exp_wids[i]);
-        g_content_explorer_delete_group(explorer, handler->exp_wids[i]);
-    }
+        for (i = 0; i < handler->exp_count; i++)
+        {
+            g_content_resolver_delete_group(resolver, handler->exp_wids[i]);
+            g_content_explorer_delete_group(explorer, handler->exp_wids[i]);
+        }
 
-    g_mutex_unlock(&handler->mutex);
+        g_object_unref(G_OBJECT(explorer));
+        g_object_unref(G_OBJECT(resolver));
 
-    g_object_unref(G_OBJECT(explorer));
-    g_object_unref(G_OBJECT(resolver));
+        /* Nettoyage plus général */
 
-    /* Nettoyage plus général */
+        g_mutex_clear(&handler->mutex);
+        g_cond_clear(&handler->wait_cond);
 
-    g_mutex_clear(&handler->mutex);
-    g_cond_clear(&handler->wait_cond);
+        g_clear_object(&handler->project);
 
-    g_object_unref(G_OBJECT(handler->project));
+    }
 
     G_OBJECT_CLASS(g_loading_handler_parent_class)->dispose(G_OBJECT(handler));
 
@@ -1089,13 +1094,61 @@ static bool g_loading_handler_check(GLoadingHandler *handler, wgroup_id_t wid)
     bool result;                        /* Bilan à retourner           */
     size_t i;                           /* Boucle de parcours          */
 
-    assert(!g_mutex_trylock(&handler->mutex));
+    /**
+     * Les deux appelants on_new_content_explored() et on_new_content_resolved()
+     * ne doivent absolument pas poser un large verrou en filtrant les identifiants
+     * via cette fonction.
+     *
+     * On pose donc ce verrou ici.
+     *
+     * La raison mineure est que les deux appelants n'accèdent qu'en lecture
+     * (à peu de chose près) aux propriétés de l'objet handler.
+     *
+     * La raison principale est la suivante :
+     *
+     *    - on_new_content_explored() crée un groupe de tâches via l'appel
+     *      g_content_resolver_create_group().
+     *
+     *    - on_new_content_resolved() peut conduire à une libération de l'objet
+     *      handler, procédure qui va libérer ensuite le groupe de tâches associé.
+     *
+     * Ce qui peut conduire à un dead lock avec un large verrou :
+     *
+     *    - un thread T1 termine une résolution wid=3 ; il va donc appeler
+     *      tous les handlers via le signal "resolved".
+     *
+     *    - T1 va rencontrer le handler qui gère wid=3. C'était la dernière
+     *      résolution, donc un broadcast sur le compteur "resolved" va
+     *      être émis.
+     *
+     *    - un thread T2 en attente dans g_loading_handler_process() va donc
+     *      terminer sa tâche. Depuis la fonction g_loading_handler_dispose(),
+     *      cette tâche va libérer le groupe associé, dont l'exécution est
+     *      assurée par T1.
+     *
+     *    - le thread T2 va donc terminer sur un g_thread_join() dans la fonction
+     *      g_work_group_dispose(), en attandant que T1 remarque l'ordre d'arrêt.
+     *
+     *    - or T1 va continuer la propagation du signal "resolved" aux autres
+     *      résolveurs (par exemple, celui gérant wid=4).
+     *
+     *    - nouvelle exécution de on_new_content_resolved(), qui bloque cette
+     *      fois, car le handler wid=4 est occupé dans un thread T3 à la fonction
+     *      on_new_content_explored(), suite à un signal "explored" avec wid=4.
+     *
+     *    - si le verrou handler->mutex est posé en même temps que la modification
+     *      des groupes de tâches, alors T1, T2 et T3 vont se bloquer mutuellement.
+     */
+
+    g_mutex_lock(&handler->mutex);
 
     result = false;
 
     for (i = 0; i < handler->exp_count && !result; i++)
         result = (handler->exp_wids[i] == wid);
 
+    g_mutex_unlock(&handler->mutex);
+
     return result;
 
 }
@@ -1122,8 +1175,6 @@ static void on_new_content_explored(GContentExplorer *explorer, wgroup_id_t wid,
     GContentResolver *resolver;             /* Resolveur de contenus       */
     size_t i;                               /* Boucle de parcours          */
 
-    g_mutex_lock(&handler->mutex);
-
     if (g_loading_handler_check(handler, wid))
     {
         available = g_content_explorer_get_all(explorer, wid, &count);
@@ -1142,8 +1193,6 @@ static void on_new_content_explored(GContentExplorer *explorer, wgroup_id_t wid,
 
     }
 
-    g_mutex_unlock(&handler->mutex);
-
 }
 
 
@@ -1173,8 +1222,6 @@ static void on_new_content_resolved(GContentResolver *resolver, wgroup_id_t wid,
     xmlXPathObjectPtr xobject;              /* Cible d'une recherche       */
     bool status;                            /* Bilan d'une restauration    */
 
-    g_mutex_lock(&handler->mutex);
-
     if (g_loading_handler_check(handler, wid))
     {
         available = g_content_resolver_get_all(resolver, wid, &count);
@@ -1268,13 +1315,15 @@ static void on_new_content_resolved(GContentResolver *resolver, wgroup_id_t wid,
 
         /* Si c'était la dernière résolution... */
 
+        g_mutex_lock(&handler->mutex);
+
         handler->resolved++;
 
         g_cond_broadcast(&handler->wait_cond);
 
-    }
+        g_mutex_unlock(&handler->mutex);
 
-    g_mutex_unlock(&handler->mutex);
+    }
 
 }
 
-- 
cgit v0.11.2-87-g4458