Export CSV plus long

Bonjour,

Suite à notre passage en version 5.2 le 09/05, nous avons remarqué une augmentation du temps de traitement concernant la génération de fichier CSV.

La fonctionnalité en cause, utilise massivement cette méthode : CSVTool

Y a-t-il eu des changements la concernant entre la 5.1 et la 5.2 ? (Le but étant d’évincer une piste si ce n’est pas le cas)

L’utilisation de cette méthode est elle toujours en 5.2 la plus efficace niveau temps de traitement ?

Merci d’avance pour vos retours,

Benoît

Technical information

Instance /health
[Platform]
Status=OK
Version=5.2.38
BuiltOn=2023-04-20 10:56
Git=5.2/66dd3f848850f0ba670a5f92674282285b3d3341
Encoding=UTF-8
TimeZone=Europe/Paris
SystemDate=2023-05-30 10:47:56

Bonjour,

Oui les exports ont fortement évolué notamment pour ne plus saturer la mémoire / pagination des recherches et flush sur disque ou directement dans la response HTTP suivant le média (export textuel CVS, ou binaire genre XLSX=ZIP forcement sur disque avant d’être envoyé).

Je ne vois pas en quoi cela ralentirait les exports (sauf les écritures disques plus longues qu’en mémoire).
Avez vous du code dans des hooks (pre-post search-select…) ?
Faites vous des tests 5.1/5.2 iso-du-reste ? même code/hook, même base/données ?

Par ailleurs pourquoi ne pas migrer/tester sur une 5.3 à jour ?

Le sujet du passage en 5.3 est prévu pour notre prochain sprint.

Oui les tests ont été fait sur des instances ISO hors version.

Le problème ne semble être présent que sur les gros exports.

Voici le morceau de code qui gère le CSV :

File f = new File(objectName + ".csv");
PrintWriter p = new PrintWriter(f);
CSVTool.export(obj, null, "list", ";", false, p);
saveFile("csv", objectName, "UPDATED", f);

En 5.1 les gros exports finissent en heap/memory exception car écrivent en mémoire avant d’être retournés à l’appelant :

PrintWriter out = new PrintWriter(new StringWriter());

  • En 5.2+ l’API demande un PrintWriter, vous lui passez un fichier (quel chemin ?) donc la différence est le temps d’écriture sur disque, ou ce que fait la suite “saveFile”…

  • Via la UI, le back écrit directement dans le flux xhr/http pour avoir la progression.

Vous pouvez continuer à lui passer un StringWriter mais au risque de saturer le heap.

1 Like

3 remarques majeures :

  • il faut préciser le chemin car par défaut, je ne sais pas où Tomcat va aller écrire ce fichier (à la racine de tomcat… ?). Pour écrire dans le répertoire tmp dans la webapp :

Platform.getTmpDir() + "/myfile.csv"

  • S’il y a 2 exports en parallèle, le nom du fichier est le même…
    il faut donc un timestamp ou un random qq part, pour ça il y a des tools :

FileTool.getRandomFile(dir, prefix, extension)

  • Pensez à supprimer le fichier à la fin de votre traitement.
1 Like

Bonjour @Francois,

J’ai intégré les modifications que tu as remontées.

J’ai cependant un problème avec la méthode CSVTool.export et la suppression du fichier.

Sauf erreur de ma part, CSVTool.export lance une méthode asynchrone pour générer le fichier.

Comment puis-je vérifier que le traitement est bien terminé avant que la suppression du fichier ne se produise ?

De façon générale, je vais refaire le développement de cette fonctionnalité qui date de deux ans, et ne réponds plus au besoin actuel, tant sur la consommation mémoire que sur la vitesse d’exécution.

Le besoin initial est celui ci :

Pouvoir exporter une 30aines d’objet métier (la liste est figée), en format CSV (si possible en //) et les stocker dans un Field de type Document pour les laisser à disposition des gens, souhaitant télécharger le ZIP ainsi obtenu.

J’ai regardé le tips and tricks de @scampano :

Auriez-vous un exemple de code permettant de générer des export CSV en // avec une consommation mémoire convenable à me partager ?

Merci d’avance,

Benoît

La nouvelle méthode CSVTool.export est synchrone, par contre elle pagine la recherche des objets et flush dans l’output stream au fur et à mesure des exports pour garder un espace mémoire limité.

Pour exporter en //, il faut veiller :

  • à exporter dans les fichiers disjoints (cf random file ou nommage qui dépende du login, date…)
  • à lancer des threads dans le pool de threads Simplicité pour en limiter le nombre en // (10 job asynchrones par défaut) : en lancer 30 en // ne sert à rien en soit car c’est le nombre de coeurs du serveur qui en le goulot d’étranglement
  • à utiliser un compteur global qui regarde si tout est terminé pour finaliser le traitement, par exemple :
int max = 30:
final AtomicInteger n = new AtomicInteger(max);
for (int i=0; i<max; i++)
  JobQueue.push("my_export_" + i, new Runnable() {
	@Override
	public void run() {
		// export object[i] in a isolated/unique file
		CSVTool.export(...);
		// all done ?
		if (n.decrementAndGet()==0) {
			// save a zip ...
		}
	}
  });
1 Like

Bonjour @Francois,

J’ai mis en place la solution, cependant je remarque dans le monitoring, que j’ai plus de 10 Jobs en //, malgré le paramètre système :

Le nombre de ces jobs ne devrait il pas se limiter à 10 max dans le state “RUNNABLE” ?

Comment s’appellent les thread dont tu parles ?
Si tu en as plus, c’est que votre code instancie peut-être des threads et pas des runnable ?

Par défaut, le pool est de 10 threads SimplicitePoolWorker qui attendent un job empilé dans le deamon SimpliciteCronThread. C’est largement suffisant car en général c’est le nombre de coeurs qui détermine la vraie limite physique.

Tu peux regarder la liste des threads Simplicité dans le monitoring en cliquant sur l’onglet “All threads” <=> “Simplicité”.

En V6, il y a 1 pool Système et 1 pool Applicatif pour ne pas bloquer les files d’attente système (GC, prune logs…) avec des applications qui consommeraient toute la bande passante.

C’est bien depuis cet écran que je tire le nombre remonté :

J’ai donné des noms distinct à mes Jobs, et j’en ai plus de 10 dans la liste au statut “RUNNABLE”.

Ne devraient ils pas rester en “WAITING” à partir du 10ième lancé ?

Merci d’avance pour tes lumières @Francois.

Pourtant j’ai bien implémenté le code :

Oui le pool est sensé être limité… un truc m’échappe.

Avant Simplicité empilait des Threads qui prenaient beaucoup de ressources système (et souvent en nombre limité au niveau Tomcat) dans un état WAITING tant que pas de file disponible, donc on pouvait en empiler des millions et planter le serveur.

On a dû changer par des Runnable invisibles par Tomcat et qui sont lancés dans un thread alloué quand une des 10 files est disponible, donc quand on les voit c’est forcement en RUNNING (en empiler des millions finira par saturer le heap mais pas tomcat).

Si le CSVTool.export était asynchrone et rendait la main, tous les runnables termineraient rapidement.

On va refaire des tests avec cet exemple de code.

1 Like

@Francois, je souhaitais aussi intégrer un :

obj.destroy();

Pour optimiser la consommation mémoire. Cependant où que je place l’instruction, je suis confronté par moment à une erreur ConcurrentModificationException.

J’ai pourtant mis un nom unique à chaque instance d’ObjectDB, et je fais le destroy après le :

CSVTool.export(…);

Ai je raté quelque chose ?

Ok on a trouvé le problème en passant dans le JobQueue.push.

A mon avis c’est lié à un correctif suite à une remontée SonarQube.

  • Avant chaque worker faisait un run() du thread/runnable = appel bloquant, mais faire un run ce n’est pas exécuter le cycle de vie du Thread, c’est juste un appel à la méthode run, c’est mal.
  • Ca a été remplacé par un start() qui lance le thread proprement de manière asynchrone mais du coup ça redonne la main au worker qui continue de dépiler trop vite !

On va ajouter un join() pour bloquer le worker et attendre la fin de la tache avant de dépiler la suivante.

image

J’ai utilisé un Thread.sleep(30s) dans l’exemple à la place de l’export.
On voit bien que les taches sont dépilées 10 par 10.
Ce sera poussé dans une prochaine révision.

1 Like

Bonjour @Francois,

Merci pour ton retour.

Cette correction sera-t-elle backporté en 5.2 ?

As tu une date en tête pour la prochaine révision contenant ce correctif ?

Merci d’avance.

Oui ça a été backporté jusqu’en V4, car le remplacement du “run” par “start” avait été fait partout.

@david une idée de date pour la maintenance 5.2 ?

Un build de toutes les versions maintenues est prévu aujourd’hui ou demain.

Pour la 5.2 il s’agira de la révision 5.2.42, ce sera annoncé dans la catégorie “Announces”

1 Like

@Francois une idée par rapport à ce problème ?

Ce ticket commence à être un fourre tout impossible à suivre.
Sans le code complet ni la stack complete, impossible de savoir à quel niveau ça bloque.
Il y a une concurrence d’accès quelque part.

Le pattern est en général le suivant :

ObjectDB obj = getGrant().getIsolatedObject("x");
// or
ObjectDB obj = getGrant().getObject("myInstanceX","x");
synchronized (obj.getLock()) {
   try {
      // ... use obj ...
   }
   finally {
      obj.destroy();
   }
}
1 Like