@@ -560,7 +560,12 @@ public DatasetPage find(int page, int size, @NonNull DatasetCriteria criteria, L
560560 }
561561
562562 // For now, we are not going to use the criteria.withExperimentsOnly() method due to the migration.
563- return template .inTransaction (READ_ONLY , handle -> {
563+ // Enrichment runs outside the transaction so the MySQL connection is not held open across the three
564+ // ClickHouse round-trips it makes. Trade-off: the page rows and the dataset versions were previously
565+ // read in one READ_ONLY transaction and are now two, so a version written between them is observable
566+ // where it was not before. Benign for count/summary metadata, and only find() is affected -- findById,
567+ // findByNameDetailed and the experiments-only branch already enriched outside any transaction.
568+ DatasetPage rawPage = template .inTransaction (READ_ONLY , handle -> {
564569
565570 var repository = handle .attach (DatasetDAO .class );
566571 int offset = (page - 1 ) * size ;
@@ -569,14 +574,17 @@ public DatasetPage find(int page, int size, @NonNull DatasetCriteria criteria, L
569574 criteria .withExperimentsOnly (),
570575 criteria .withOptimizationsOnly (), visibility , filtersSQL , filterMapping );
571576
572- List <Dataset > datasets = enrichDatasetWithAdditionalInformation (
573- repository .find (size , offset , workspaceId , criteria .name (), criteria .projectId (),
574- criteria .withExperimentsOnly (),
575- criteria .withOptimizationsOnly (),
576- sortingFieldsSql , visibility , filtersSQL , filterMapping ));
577+ List <Dataset > datasets = repository .find (size , offset , workspaceId , criteria .name (), criteria .projectId (),
578+ criteria .withExperimentsOnly (),
579+ criteria .withOptimizationsOnly (),
580+ sortingFieldsSql , visibility , filtersSQL , filterMapping );
577581
578582 return new DatasetPage (datasets , page , datasets .size (), count , sortingFactory .getSortableFields ());
579583 });
584+
585+ List <Dataset > datasets = enrichDatasetWithAdditionalInformation (rawPage .content ());
586+
587+ return new DatasetPage (datasets , page , datasets .size (), rawPage .total (), sortingFactory .getSortableFields ());
580588 }
581589
582590 private Mono <DatasetPage > fetchUsingTempTable (int page , int size , DatasetCriteria criteria , Set <UUID > ids ,
@@ -708,31 +716,47 @@ public List<UUID> findIdsByPartialName(@NonNull String workspaceId, @NonNull Str
708716 private List <Dataset > enrichDatasetWithAdditionalInformation (List <Dataset > datasets ) {
709717 Set <UUID > ids = datasets .stream ().map (Dataset ::id ).collect (toSet ());
710718
711- Map <UUID , ExperimentSummary > experimentSummary = experimentItemDAO .findExperimentSummaryByDatasetIds (ids )
712- .contextWrite (ctx -> AsyncUtils .setRequestContext (ctx , requestContext ))
713- .toStream ()
714- .collect (toMap (ExperimentSummary ::datasetId , Function .identity ()));
715-
716- Map <UUID , DatasetItemSummary > datasetItemSummaryMap = datasetItemDAO .findDatasetItemSummaryByDatasetIds (ids )
717- .contextWrite (ctx -> AsyncUtils .setRequestContext (ctx , requestContext ))
718- .toStream ()
719- .collect (toMap (DatasetItemSummary ::datasetId , Function .identity ()));
719+ if (ids .isEmpty ()) {
720+ return datasets ;
721+ }
720722
721- Map <UUID , OptimizationDAO .OptimizationSummary > optimizationSummaryMap = optimizationDAO
722- .findOptimizationSummaryByDatasetIds (ids )
723- .contextWrite (ctx -> AsyncUtils .setRequestContext (ctx , requestContext ))
724- .toStream ()
725- .collect (toMap (OptimizationDAO .OptimizationSummary ::datasetId , Function .identity ()));
723+ // RequestContext is @RequestScoped and therefore thread-bound: resolve it here, on the request thread,
724+ // so the concurrent subscriptions below never call requestContext.get() from a worker thread.
725+ String workspaceId = requestContext .get ().getWorkspaceId ();
726+ String userName = requestContext .get ().getUserName ();
726727
727- Map <UUID , DatasetVersion > latestVersionsByDatasetId = fetchLatestVersionsByDatasetIds (ids );
728+ // collect(...) with Collectors.toMap rather than Flux.collectMap: collectMap is last-wins, whereas the
729+ // serial code this replaces threw on a duplicate dataset_id. All three queries GROUP BY dataset_id so
730+ // duplicates should not occur; keeping the loud form means a query change that broke that assumption
731+ // fails instead of silently dropping one row's summary.
732+ var enrichmentData = Mono .zip (
733+ experimentItemDAO .findExperimentSummaryByDatasetIds (ids )
734+ .collect (toMap (ExperimentSummary ::datasetId , Function .identity ())),
735+ datasetItemDAO .findDatasetItemSummaryByDatasetIds (ids )
736+ .collect (toMap (DatasetItemSummary ::datasetId , Function .identity ())),
737+ optimizationDAO .findOptimizationSummaryByDatasetIds (ids )
738+ .collect (toMap (OptimizationDAO .OptimizationSummary ::datasetId , Function .identity ())),
739+ // defaultIfEmpty guards the zip: a Mono that completes empty makes zip emit nothing at all,
740+ // which would turn an absent-versions result into a null and NPE below.
741+ Mono .fromCallable (() -> fetchLatestVersionsByDatasetIds (ids , workspaceId ))
742+ .subscribeOn (Schedulers .boundedElastic ())
743+ .defaultIfEmpty (Map .of ()))
744+ .contextWrite (ctx -> AsyncUtils .setRequestContext (ctx , userName , workspaceId ))
745+ .block ();
746+
747+ Map <UUID , ExperimentSummary > experimentSummaryMap = enrichmentData .getT1 ();
748+ Map <UUID , DatasetItemSummary > datasetItemSummaryMap = enrichmentData .getT2 ();
749+ Map <UUID , OptimizationDAO .OptimizationSummary > optimizationSummaryMap = enrichmentData .getT3 ();
750+ Map <UUID , DatasetVersion > latestVersionsByDatasetId = enrichmentData .getT4 ();
728751
729752 return datasets .stream ()
730753 .map (dataset -> {
731- var resume = experimentSummary .computeIfAbsent (dataset .id (), ExperimentSummary ::empty );
732- var datasetItemSummary = datasetItemSummaryMap .computeIfAbsent (dataset .id (),
733- DatasetItemSummary ::empty );
734- var optimizationSummary = optimizationSummaryMap .computeIfAbsent (dataset .id (),
735- OptimizationDAO .OptimizationSummary ::empty );
754+ var experimentSummary = experimentSummaryMap .getOrDefault (dataset .id (),
755+ ExperimentSummary .empty (dataset .id ()));
756+ var datasetItemSummary = datasetItemSummaryMap .getOrDefault (dataset .id (),
757+ DatasetItemSummary .empty (dataset .id ()));
758+ var optimizationSummary = optimizationSummaryMap .getOrDefault (dataset .id (),
759+ OptimizationDAO .OptimizationSummary .empty (dataset .id ()));
736760 var latestVersion = latestVersionsByDatasetId .get (dataset .id ());
737761
738762 // When versioning is enabled and a latest version exists, use itemsTotal from the version
@@ -746,24 +770,22 @@ private List<Dataset> enrichDatasetWithAdditionalInformation(List<Dataset> datas
746770 }
747771
748772 return dataset .toBuilder ()
749- .experimentCount (resume .experimentCount ())
773+ .experimentCount (experimentSummary .experimentCount ())
750774 .datasetItemsCount (itemsCount )
751775 .optimizationCount (optimizationSummary .optimizationCount ())
752- .mostRecentExperimentAt (resume .mostRecentExperimentAt ())
776+ .mostRecentExperimentAt (experimentSummary .mostRecentExperimentAt ())
753777 .mostRecentOptimizationAt (optimizationSummary .mostRecentOptimizationAt ())
754778 .latestVersion (DatasetVersionMapper .INSTANCE .toDatasetVersionSummary (latestVersion ))
755779 .build ();
756780 })
757781 .toList ();
758782 }
759783
760- private Map <UUID , DatasetVersion > fetchLatestVersionsByDatasetIds (Set <UUID > datasetIds ) {
784+ private Map <UUID , DatasetVersion > fetchLatestVersionsByDatasetIds (Set <UUID > datasetIds , String workspaceId ) {
761785 if (datasetIds .isEmpty ()) {
762786 return Map .of ();
763787 }
764788
765- String workspaceId = requestContext .get ().getWorkspaceId ();
766-
767789 return template .inTransaction (READ_ONLY , handle -> {
768790 var dao = handle .attach (DatasetVersionDAO .class );
769791 List <DatasetVersion > latestVersions = dao .findLatestVersionsByDatasetIds (datasetIds , workspaceId );
0 commit comments