This is an automated email from the ASF dual-hosted git repository.

korlov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new c2afff94d8 IGNITE-19412 Sql. Memory leak in MailboxRegistryImpl (#2043)
c2afff94d8 is described below

commit c2afff94d80f479a4e6099ab6e126efe9b232a17
Author: korlov42 <[email protected]>
AuthorDate: Thu May 11 16:49:19 2023 +0300

    IGNITE-19412 Sql. Memory leak in MailboxRegistryImpl (#2043)
---
 .../sql/engine/exec/ExecutionServiceImpl.java      | 159 ++++++++++++---------
 .../sql/engine/exec/ExecutionServiceImplTest.java  |  81 ++++++++++-
 2 files changed, 174 insertions(+), 66 deletions(-)

diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java
index 06b75885a3..062d53155a 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java
@@ -218,7 +218,7 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
             BaseQueryContext ctx,
             MultiStepPlan plan
     ) {
-        DistributedQueryManager queryManager = new 
DistributedQueryManager(ctx);
+        DistributedQueryManager queryManager = new 
DistributedQueryManager(true, ctx);
 
         DistributedQueryManager old = queryManagerMap.put(ctx.queryId(), 
queryManager);
 
@@ -427,6 +427,8 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
      * A convenient class that manages the initialization and termination of 
distributed queries.
      */
     private class DistributedQueryManager {
+        private final boolean coordinator;
+
         private final BaseQueryContext ctx;
 
         private final CompletableFuture<Void> cancelFut = new 
CompletableFuture<>();
@@ -441,9 +443,9 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
 
         private volatile Long rootFragmentId = null;
 
-
-        private DistributedQueryManager(BaseQueryContext ctx) {
+        private DistributedQueryManager(boolean coordinator, BaseQueryContext 
ctx) {
             this.ctx = ctx;
+            this.coordinator = coordinator;
 
             var root = new CompletableFuture<AsyncRootNode<RowT, 
List<Object>>>();
 
@@ -456,6 +458,10 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
             this.root = root;
         }
 
+        private DistributedQueryManager(BaseQueryContext ctx) {
+            this(false, ctx);
+        }
+
         private List<AbstractNode<?>> localFragments() {
             return List.copyOf(localFragments);
         }
@@ -660,32 +666,32 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
                         for (String nodeName : fragmentDesc.nodeNames()) {
                             CompletableFuture<Void> resultOfSending = 
sendFragment(nodeName, fragment, fragmentDesc, attributes);
 
-                            resultsOfFragmentSending.add(resultOfSending);
-
-                            resultOfSending.handle((ignored, t) -> {
-                                if (t == null) {
-                                    return null;
-                                }
+                            resultsOfFragmentSending.add(
+                                    resultOfSending.handle((ignored, t) -> {
+                                        if (t == null) {
+                                            return null;
+                                        }
 
-                                // if we were unable to send a request, then 
no need
-                                // to wait for the remote node to complete 
initialization
+                                        // if we were unable to send a 
request, then no need
+                                        // to wait for the remote node to 
complete initialization
 
-                                CompletableFuture<?> completionFuture = 
remoteFragmentInitCompletion.get(
-                                        new RemoteFragmentKey(nodeName, 
fragment.fragmentId())
-                                );
+                                        CompletableFuture<?> completionFuture 
= remoteFragmentInitCompletion.get(
+                                                new 
RemoteFragmentKey(nodeName, fragment.fragmentId())
+                                        );
 
-                                if (completionFuture != null) {
-                                    completionFuture.complete(null);
-                                }
+                                        if (completionFuture != null) {
+                                            completionFuture.complete(null);
+                                        }
 
-                                throw ExceptionUtils.withCauseAndCode(
-                                        IgniteInternalException::new,
-                                        Sql.INTERNAL_ERR,
-                                        format("Unable to send fragment 
[targetNode={}, fragmentId={}, cause={}]",
-                                                nodeName, 
fragment.fragmentId(), t.getMessage()),
-                                        t
-                                );
-                            });
+                                        throw ExceptionUtils.withCauseAndCode(
+                                                IgniteInternalException::new,
+                                                Sql.INTERNAL_ERR,
+                                                format("Unable to send 
fragment [targetNode={}, fragmentId={}, cause={}]",
+                                                        nodeName, 
fragment.fragmentId(), t.getMessage()),
+                                                t
+                                        );
+                                    })
+                            );
                         }
                     }
 
@@ -812,56 +818,23 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
 
             start
                     .thenCompose(tmp -> {
-                        Map<String, List<CompletableFuture<?>>> 
requestsPerNode = new HashMap<>();
-                        for (Map.Entry<RemoteFragmentKey, 
CompletableFuture<Void>> entry : remoteFragmentInitCompletion.entrySet()) {
-                            
requestsPerNode.computeIfAbsent(entry.getKey().nodeName(), key -> new 
ArrayList<>()).add(entry.getValue());
-                        }
-
-                        List<CompletableFuture<?>> cancelFuts = new 
ArrayList<>();
-                        for (Map.Entry<String, List<CompletableFuture<?>>> 
entry : requestsPerNode.entrySet()) {
-                            String nodeId = entry.getKey();
-
-                            cancelFuts.add(
-                                    
CompletableFuture.allOf(entry.getValue().toArray(new CompletableFuture[0]))
-                                            .handle((none2, t) -> {
-                                                // Some fragments may be 
completed exceptionally, and that is fine.
-                                                // Here we need to make sure 
that all query initialisation requests
-                                                // have been processed before 
sending a cancellation request. That's
-                                                // why we just ignore any 
exception.
-
-                                                return null;
-                                            })
-                                            .thenCompose(ignored -> 
messageService.send(
-                                                    nodeId,
-                                                    FACTORY.queryCloseMessage()
-                                                            
.queryId(ctx.queryId())
-                                                            .build()
-                                            ))
-                            );
-                        }
-
-                        if (cancel) {
-                            ExecutionCancelledException ex = new 
ExecutionCancelledException();
-
-                            for (AbstractNode<?> node : localFragments) {
-                                node.context().execute(() -> node.onError(ex), 
node::onError);
-                            }
-                        }
+                        CompletableFuture<Void> cancelResult = coordinator
+                                ? awaitFragmentInitialisationAndClose()
+                                : closeLocalFragments();
 
-                        var compoundCancelFut = 
CompletableFuture.allOf(cancelFuts.toArray(new CompletableFuture[0]));
-                        var finalStepFut = compoundCancelFut.whenComplete((r, 
e) -> {
+                        var finalStepFut = cancelResult.whenComplete((r, e) -> 
{
                             queryManagerMap.remove(ctx.queryId());
 
                             try {
                                 ctx.cancel().cancel();
-                            } catch (Exception ex) {
+                            } catch (Exception ignored) {
                                 // NO-OP
                             }
 
                             cancelFut.complete(null);
                         });
 
-                        return compoundCancelFut.thenCombine(finalStepFut, 
(none1, none2) -> null);
+                        return cancelResult.thenCombine(finalStepFut, (none1, 
none2) -> null);
                     });
 
             start.completeAsync(() -> null, taskExecutor);
@@ -869,6 +842,64 @@ public class ExecutionServiceImpl<RowT> implements 
ExecutionService, TopologyEve
             return cancelFut.thenApply(Function.identity());
         }
 
+        private CompletableFuture<Void> closeLocalFragments() {
+            ExecutionCancelledException ex = new ExecutionCancelledException();
+
+            List<CompletableFuture<?>> localFragmentCompletions = new 
ArrayList<>();
+            for (AbstractNode<?> node : localFragments) {
+                if (node.context().isCancelled()) {
+                    continue;
+                }
+
+                localFragmentCompletions.add(
+                        node.context().submit(() -> node.onError(ex), 
node::onError)
+                );
+            }
+
+            return CompletableFuture.allOf(
+                    localFragmentCompletions.toArray(new CompletableFuture[0])
+            );
+        }
+
+        private CompletableFuture<Void> awaitFragmentInitialisationAndClose() {
+            Map<String, List<CompletableFuture<?>>> requestsPerNode = new 
HashMap<>();
+            for (Map.Entry<RemoteFragmentKey, CompletableFuture<Void>> entry : 
remoteFragmentInitCompletion.entrySet()) {
+                requestsPerNode.computeIfAbsent(entry.getKey().nodeName(), key 
-> new ArrayList<>()).add(entry.getValue());
+            }
+
+            List<CompletableFuture<?>> cancelFuts = new ArrayList<>();
+            for (Map.Entry<String, List<CompletableFuture<?>>> entry : 
requestsPerNode.entrySet()) {
+                String nodeId = entry.getKey();
+
+                cancelFuts.add(
+                        CompletableFuture.allOf(entry.getValue().toArray(new 
CompletableFuture[0]))
+                                .handle((none2, t) -> {
+                                    // Some fragments may be completed 
exceptionally, and that is fine.
+                                    // Here we need to make sure that all 
query initialisation requests
+                                    // have been processed before sending a 
cancellation request. That's
+                                    // why we just ignore any exception.
+
+                                    return null;
+                                })
+                                .thenCompose(ignored -> {
+                                    // for local fragments don't send the 
message, just close the fragments
+                                    if (localNode.name().equals(nodeId)) {
+                                        return closeLocalFragments();
+                                    }
+
+                                    return messageService.send(
+                                            nodeId,
+                                            FACTORY.queryCloseMessage()
+                                                    .queryId(ctx.queryId())
+                                                    .build()
+                                    );
+                                })
+                );
+            }
+
+            return CompletableFuture.allOf(cancelFuts.toArray(new 
CompletableFuture[0]));
+        }
+
         /**
          * Synchronously closes the tree's execution iterator.
          *
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
index 305b4b17d7..a9d34fcde6 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
@@ -39,9 +39,12 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
@@ -64,7 +67,9 @@ import 
org.apache.ignite.internal.sql.engine.AsyncCursor.BatchedResult;
 import org.apache.ignite.internal.sql.engine.QueryCancel;
 import 
org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImplTest.TestCluster.TestNode;
 import org.apache.ignite.internal.sql.engine.exec.ddl.DdlCommandHandler;
+import org.apache.ignite.internal.sql.engine.exec.rel.Inbox;
 import org.apache.ignite.internal.sql.engine.exec.rel.Node;
+import org.apache.ignite.internal.sql.engine.exec.rel.Outbox;
 import org.apache.ignite.internal.sql.engine.exec.rel.ScanNode;
 import org.apache.ignite.internal.sql.engine.framework.NoOpTransaction;
 import org.apache.ignite.internal.sql.engine.framework.TestTable;
@@ -129,6 +134,8 @@ public class ExecutionServiceImplTest {
 
     private final IgniteSchema schema = new IgniteSchema("PUBLIC", 
Map.of(table.name(), table), null, -1);
 
+    private final List<CapturingMailboxRegistry> mailboxes = new ArrayList<>();
+
     private TestCluster testCluster;
     private List<ExecutionServiceImpl<?>> executionServices;
     private PrepareService prepareService;
@@ -146,6 +153,12 @@ public class ExecutionServiceImplTest {
     @AfterEach
     public void tearDown() throws Exception {
         prepareService.stop();
+
+        for (CapturingMailboxRegistry mailbox : mailboxes) {
+            assertTrue(waitForCondition(mailbox::empty, TIMEOUT_IN_MS));
+        }
+
+        mailboxes.clear();
     }
 
     /**
@@ -427,7 +440,7 @@ public class ExecutionServiceImplTest {
         AsyncCursor<List<Object>> cursor = execService.executePlan(tx, plan, 
ctx);
 
         // Wait till the query fails due to nodes' unavailability.
-        assertThat(cursor.closeAsync(), willThrow(hasProperty("message", 
containsString("Connection refused to ")), 10, TimeUnit.SECONDS));
+        assertThat(cursor.closeAsync(), willThrow(hasProperty("message", 
containsString("Unable to send fragment")), 10, TimeUnit.SECONDS));
 
         // Let the root fragment be executed.
         queryFailedLatch.countDown();
@@ -455,7 +468,8 @@ public class ExecutionServiceImplTest {
         node.dataset(dataPerNode.get(nodeName));
 
         var messageService = node.messageService();
-        var mailboxRegistry = new MailboxRegistryImpl();
+        var mailboxRegistry = new CapturingMailboxRegistry(new 
MailboxRegistryImpl());
+        mailboxes.add(mailboxRegistry);
 
         var exchangeService = new ExchangeServiceImpl(mailboxRegistry, 
messageService);
 
@@ -729,4 +743,67 @@ public class ExecutionServiceImplTest {
             }
         };
     }
+
+    private static class CapturingMailboxRegistry implements MailboxRegistry {
+        private final MailboxRegistry delegate;
+
+        private final Set<Inbox<?>> inboxes = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        private final Set<Outbox<?>> outboxes = Collections.newSetFromMap(new 
IdentityHashMap<>());
+
+        CapturingMailboxRegistry(MailboxRegistry delegate) {
+            this.delegate = delegate;
+        }
+
+        boolean empty() {
+            return inboxes.isEmpty() && outboxes.isEmpty();
+        }
+
+        @Override
+        public void start() {
+            delegate.start();
+        }
+
+        @Override
+        public void stop() throws Exception {
+            delegate.stop();
+        }
+
+        @Override
+        public void register(Inbox<?> inbox) {
+            delegate.register(inbox);
+
+            inboxes.add(inbox);
+        }
+
+        @Override
+        public void register(Outbox<?> outbox) {
+            delegate.register(outbox);
+
+            outboxes.add(outbox);
+        }
+
+        @Override
+        public void unregister(Inbox<?> inbox) {
+            delegate.unregister(inbox);
+
+            inboxes.remove(inbox);
+        }
+
+        @Override
+        public void unregister(Outbox<?> outbox) {
+            delegate.unregister(outbox);
+
+            outboxes.remove(outbox);
+        }
+
+        @Override
+        public CompletableFuture<Outbox<?>> outbox(UUID qryId, long 
exchangeId) {
+            return delegate.outbox(qryId, exchangeId);
+        }
+
+        @Override
+        public Inbox<?> inbox(UUID qryId, long exchangeId) {
+            return delegate.inbox(qryId, exchangeId);
+        }
+    }
 }

Reply via email to