codeconsole commented on code in PR #16494:
URL: https://github.com/apache/grails-core/pull/16494#discussion_r4175785450


##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -735,6 +747,88 @@ public void buildIndex() {
         }
     }
 
+    /**
+     * Runs the same build as {@link #buildIndex()} on this connection's 
background index build thread, whatever
+     * {@code grails.mongodb.buildIndexesAsync} says, and returns at once with 
a handle on its outcome.
+     *
+     * <p>The future completes with the build's {@link IndexBuildResult} once 
every declaration has been applied,
+     * including when some of them failed: those are counted in {@link 
IndexBuildResult#failures()} and logged
+     * as they happen. It completes exceptionally with the exception that 
stopped the build partway, such as a
+     * lost connection or a write concern the server could not satisfy. 
Created and already-present indexes are always told apart, whatever the log 
level.
+     *
+     * <p>Builds on a connection run one at a time, so a build requested while 
another is running waits for it,
+     * including one running on a caller's thread with {@link #buildIndex()}.
+     * Cancelling the future does not stop the build. If the datastore is 
stopped or closed, the future completes
+     * exceptionally at once, and a build that stopping or closing the 
datastore cuts short completes
+     * exceptionally too; a restart runs the cut-short build again, without a 
future. Each named connection
+     * builds its own domain classes: call this on {@link 
#getDatastoreForConnection(String)} for those.
+     *
+     * @return the outcome of the build
+     */
+    public CompletableFuture<IndexBuildResult> buildIndexAsync() {
+        CompletableFuture<IndexBuildResult> result = new CompletableFuture<>();
+        if (closed || !submitIndexBuild(result)) {
+            result.completeExceptionally(new IllegalStateException("The index 
build for connection [" +
+                    connectionName() + "] was not started: the datastore is " 
+ (closed ? "closed" : "stopped") + "."));
+        }
+        return result;
+    }
+
+    /**
+     * Hands a build to this connection's background index build thread.
+     *
+     * @param result the future to complete with the build's outcome, or 
{@code null} if nothing is waiting on it
+     * @return false if the executor has been shut down, by {@link #stop()} or 
{@link #close()}
+     */
+    private boolean submitIndexBuild(CompletableFuture<IndexBuildResult> 
result) {
+        ExecutorService executor = this.indexBuildExecutor;
+        if (executor.isShutdown()) {
+            return false;
+        }
+        try {
+            executor.execute(new IndexBuildTask(result, () -> {
+                // The first thing the build does: said only of a build that 
is under way, and ahead of
+                // everything it logs, which it would not be if the submitting 
thread said it.
+                LOG.info("Building the indexes declared by the domain classes 
for connection [{}] on a " +
+                        "background thread. Startup does not wait for them, so 
a query issued before its index " +

Review Comment:
   Done in cd08a7d289. A build `buildIndexAsync()` started now logs "Building 
the indexes declared by the domain classes for connection [default] on a 
background thread, for a caller waiting on the result."; a build nothing waits 
on keeps the original line. `BuildIndexAsyncResultSpec` checks that the waiting 
case logs the new line and not "Startup does not wait".
   



##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -693,7 +717,9 @@ public ConnectionSources<MongoClient, 
MongoConnectionSourceSettings> getConnecti
      * <p>Each index is created by a command that the server answers only once 
the index has been built,
      * so this blocks the calling thread for as long as MongoDB takes to build 
every declared index. With
      * {@code grails.mongodb.buildIndexesAsync} enabled the work goes to a 
background thread and this
-     * returns immediately instead.
+     * returns immediately instead. {@link #buildIndexAsync()} runs it in the 
background whatever the setting,
+     * and reports the outcome to its caller. Builds on a connection run one 
at a time, wherever they run: a

Review Comment:
   Took the lock rather than qualifying the sentences: 83c8061963. 
`persistentEntityAdded` now indexes after any build running on the connection, 
so the registering thread waits out a running build; both Javadoc sentences and 
the guide say so. `BuildIndexAsyncResultSpec` holds a `buildIndexAsync()` build 
inside index creation and checks that a class registered on another thread does 
not enter it until the build is released.
   
   The connection check is 2c4d2ca19b: `persistentEntityAdded` and 
`indexedEntities()` now share `isIndexedHere()`, which also skips external and 
schema multi-tenant classes, as the build does.
   
   Writing the test for it turned up an older bug in the same path, fixed in 
c57d0557b0. The datastore added itself as a mapping-context listener before the 
listener that resolves an entity's mapped collection and database, so a class 
registered at runtime was indexed first: `getCollectionName` and 
`getDatabaseName` fell back to the class's default collection and the default 
database, cached them, and the indexes were built there. The datastore now adds 
itself after that listener. `UndeclaredIndexesSpec` registers a class with its 
own `collection` and one with its own `database`; each assertion fails with the 
old listener order.
   



##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -902,6 +1023,236 @@ private void buildDeclaredIndexes(IndexBuildSummary 
summary) {
         }
     }
 
+    /**
+     * The entities whose declared indexes this datastore builds: those mapped 
to its connection. Every connection
+     * shares one mapping context, so an entity mapped only to another 
connection is in it too.
+     */
+    private List<PersistentEntity> indexedEntities() {
+        String connection = connectionName();
+        List<PersistentEntity> entities = new ArrayList<>();
+        for (PersistentEntity entity : 
this.mappingContext.getPersistentEntities()) {
+            // Only create Mongo templates for entities that are mapped with 
Mongo
+            if (!entity.isExternal() &&
+                    !(entity.isMultiTenant() && multiTenancyMode == 
MultiTenancySettings.MultiTenancyMode.SCHEMA) &&
+                    ConnectionSourcesSupport.usesConnectionSource(entity, 
connection)) {
+                entities.add(entity);
+            }
+        }
+        return entities;
+    }
+
+    /**
+     * The collections whose declared indexes this datastore builds, each with 
the declarations of every class
+     * mapped to it: every class in an inheritance hierarchy maps to its 
root's collection, and several classes
+     * can name the same one.
+     */
+    private Map<MongoNamespace, CollectionDeclarations> 
declarationsByCollection() {
+        Map<MongoNamespace, CollectionDeclarations> collections = new 
LinkedHashMap<>();
+        for (PersistentEntity entity : indexedEntities()) {
+            com.mongodb.client.MongoCollection<Document> collection = 
getCollection(entity);
+            CollectionDeclarations declarations = 
collections.computeIfAbsent(collection.getNamespace(),
+                    namespace -> new CollectionDeclarations(collection, new 
ArrayList<>()));
+            for (IndexDeclaration declaration : declaredIndexes(entity)) {
+                declarations.declarations().add(new EntityDeclaration(entity, 
declaration));
+            }
+        }
+        return collections;
+    }
+
+    private record 
CollectionDeclarations(com.mongodb.client.MongoCollection<Document> collection,
+                                          List<EntityDeclaration> 
declarations) {
+    }
+
+    private record EntityDeclaration(PersistentEntity entity, IndexDeclaration 
declaration) {
+    }
+
+    /**
+     * Lists the indexes the domain classes declare that their collections do 
not have, on the collections
+     * whose declared indexes {@link #buildIndex()} builds for this 
datastore's connection.
+     *
+     * <p>A declared index is present when its collection has an index on the 
same key pattern, the same fields
+     * in the same order, whatever its name and options: an option that 
differs is the build's to reconcile.
+     * A text index is present when one indexes the same text fields, in any 
order, with the same keys before and
+     * after them, whatever its weights. Keys that several classes mapped to 
one collection declare are reported
+     * once. Nothing is changed:
+     * {@link #buildIndex()} or {@link #buildIndexAsync()} creates them. Each 
named connection has its own
+     * domain classes, so call this on {@link 
#getDatastoreForConnection(String)} for those.
+     *
+     * @return the missing indexes, collection by collection, in the order the 
domain classes declare them
+     */
+    public List<MissingIndex> findMissingIndexes() {
+        List<MissingIndex> missing = new ArrayList<>();
+        for (Map.Entry<MongoNamespace, CollectionDeclarations> entry : 
declarationsByCollection().entrySet()) {
+            MongoNamespace namespace = entry.getKey();
+            List<Document> existing = 
entry.getValue().collection().listIndexes().into(new ArrayList<>());

Review Comment:
   Done in d5b93889ec. `findMissingIndexes()` skips a collection with no 
declarations. `MissingIndexesSpec` counts the `listIndexes` calls through 
`FailingMongoClient` and checks such a collection is not listed.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to