SPW-DIG / metawal-core-geonetwork

Metawal - Catalogue pour l'information géographique de Wallonie
http://metawal.wallonie.be
GNU General Public License v2.0
3 stars 1 forks source link

Module de gestion de tâches #815

Open christophenoel opened 1 year ago

christophenoel commented 1 year ago

Plusieurs tickets (#739 , #626, ...) reposent sur le besoin de gérer des tâches longues (édition de fiches en groupe).

Une solution générique réutilisable par d'autres fonctions peut être apportée en élaborant:

Dans le cadre du projet OGC Testbed-18 Secure Asynchronous Catalog Engineering Report, Christophe a élaboré en 2022 les spécifications d'un catalogue asynchrone basé sur ses travaux précédents pour des systèmes de catalogues et de traitement de données. Le rapport est disponible sur https://docs.ogc.org/per/22-018.html#toc28

Une analyse préliminaire visera à présenter plusieurs approaches (simplifiée et adaptée à GeoNetwork) inspirée de ces travaux.

Si l'approche est validée, un prototype fonctionnel peut être implémenté avec les fonctionnalité de base (et potentiellement étendu dans le future). La solution pourra être validée avec l'amélioration du traitement par lot d'édition de fiche (#739).

christophenoel commented 1 year ago

Après analyse, une solution unique et appropriée est proposée pour prendre en charge les tâches asynchrones sur MetaWal.

La gestion des requêtes à traitement prolongé (qui ne sont pas prêtes dans le délai habituel de HTTP, c'est-à-dire environ 30 secondes) peut être abordée en utilisant:

  1. un modèle de communication simple conforme à la RFC 7240.
  2. un module Spring d'exécution de tâches asynchrones

API conforme à RFC 7240

La norme proposée RFC 7240 "Prefer Header for HTTP" recommande d'utiliser l'en-tête "Prefer" pour indiquer le comportement du serveur préféré par le client. La préférence "respond-async" indique que le client préfère que le serveur réponde de manière asynchrone à une réponse. De plus, la valeur respond-async, wait=10 est un indice pour le serveur que le client s'attend à un maximum de 10 secondes pour renvoyer la réponse selon le modèle synchrone traditionnel.

La RFC 7240 mentionne que le serveur respectant la préférence "respond-async" doit renvoyer une réponse 202 (Accepté) conformément à HTTP 1.1 (RFC 7231). La représentation envoyée avec cette réponse doit décrire l'état actuel de la demande et pointer vers (ou intégrer) un moniteur d'état qui peut fournir à l'utilisateur une estimation du moment où la demande sera satisfaite.

Le serveur doit renvoyer un en-tête de localisation (Location) contenant un lien vers les ressources demandées. Le lien vers les ressources cibles peut surveiller l'état de la demande précédente jusqu'à ce que les ressources soient disponibles. La structure de la réponse du moniteur dépend de l'opération spécifique, mais est structurée sur la base d'un schéma commun.

Nous recommandons le comportement détaillé suivant:

A. Si une requête est accompagnée de l'en-tête HTTP Prefer indiquant une préférence respond-async, alors le serveur doit respecter cette préférence et répondre de manière asynchrone. B. Si une requête est accompagnée de l'en-tête HTTP Prefer, alors dans la réponse, les serveurs doivent inclure l'en-tête de réponse HTTP Preference-Applied comme indication des jetons "Prefer" honorés par le serveur. C. Si une requête est exécutée de manière asynchrone, le serveur doit répondre avec un code de statut HTTP 202. Le serveur doit renvoyer un en-tête de localisation (et un en-tête facultatif Retry-After) contenant un lien vers le travail surveillant le traitement de la requête. D. Le serveur doit prendre en charge l'opération HTTP GET pour récupérer un travail asynchrone de longue durée sur le chemin /jobs/{job

Le schéma jobStatus, qui sert de base pour la réponse en cas d'exécution réussie de l'opération, est présenté ci-dessous pour référence:

"JobStatus": {
            "type": "object",
            "properties": {
                "id": {
                    "description": "Identifiant unique de la tache",
                    "type": "string"
                },
                "status": {
                    "description": "Status actuel de la tâche (accepted, running, failed, dismissed, successful)",
                    "type": "string"
                },
                "progress": {
                    "description": "Une indication général de la progression de la tâche",
                    "type": "string"
                },
                "start-time": {
                    "description": "Heure de démarrage de la tâche (format ISO)",
                    "type": "string",
                    "format": "date-time"
                },
                "end-time": {
                    "description": "Heure de fin de la tâche (format ISO)",
                    "type": "string",
                    "format": "date-time"
                },
                "result": {
                    "description": "Conteneur des résultats (présent si status=successful)",
                    "type": "object",
                    "properties": {
                        "href": {
                            "description": "Propriété suggérée pour une référence vers les resources produites.",
                            "type": "array",
                            "items": {
                                "type": "string",
                                "format": "uri"
                            }
                        }
                    },
                    "additionalProperties": {
                        "description": "Extensions possible pour l'opération spéifique"
                    }
                },
                "error": {
                    "description": "Conteneur des mesages d'erreurs",
                    "type": "object"
                }
            },
            "additionalProperties": false
        }

Voici la liste des statuts possibles avec un texte plus court :

Module Spring pour tâches asynchrones

L'architecture du module AsyncTaskModule est conçue pour faciliter l'exécution de tâches asynchrones dans une application Spring. Elle s'appuie sur l'abstraction du module Spring Task Execution et permet de définir et d'exécuter des tâches asynchrones en implémentant une interface spécifique. Voici les composants clés et leurs responsabilités dans cette architecture :

Voici l'interface AsyncTask qui définira les méthodes pour les tâches et pour retourner leur statut basé sur le schéma JSON donné :

public interface AsyncTask {

    String getId();

    String getStatus();

    String getProgress();

    String getStartTime();

    String getEndTime();

    Object getResult();

    Object getError();

    void execute();
}

Voici un brouillon pour le module AsyncTaskModule qui supporte les méthodes demandées :

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class AsyncTaskModule {

    private final ThreadPoolTaskExecutor taskExecutor;
    private final Map<String, AsyncTask> taskMap;

    public AsyncTaskModule(ThreadPoolTaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
        this.taskMap = new ConcurrentHashMap<>();
    }

    public void submitTask(AsyncTask task) {
        taskMap.put(task.getId(), task);
        taskExecutor.execute(task::execute);
    }

    public Optional<JobStatus> getStatus(String taskId) {
        AsyncTask task = taskMap.get(taskId);
        if (task != null) {
            return Optional.of(new JobStatus(task.getStatus(), task.getProgress()));
        }
        return Optional.empty();
    }

    public boolean cancelTask(String taskId) {
        AsyncTask task = taskMap.get(taskId);
        if (task != null && task.getStatus().equals("running")) {
            task.cancel();
            taskMap.put(taskId, task);
            return true;
        }
        return false;
    }

    public boolean deleteTask(String taskId) {
        AsyncTask task = taskMap.get(taskId);
        if (task != null && (task.getStatus().equals("failed") || task.getStatus().equals("cancelled") || task.getStatus().equals("successful"))) {
            taskMap.remove(taskId);
            return true;
        }
        return false;
    }

    private static class JobStatus {
        private final String status;
        private final String progress;

        public JobStatus(String status, String progress) {
            this.status = status;
            this.progress = progress;
        }

        // Getters (et éventuellement