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

gianm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 3cedc8ad05c Discover Dart enabled historicals via a new 
DartWorkerService instead of inferring workers solely off service == historical 
(#20508)
3cedc8ad05c is described below

commit 3cedc8ad05cc173905a08555ff4e3d371244e45a
Author: Lucas Capistrant <[email protected]>
AuthorDate: Thu Oct 8 17:05:54 2026 -0500

    Discover Dart enabled historicals via a new DartWorkerService instead of 
inferring workers solely off service == historical (#20508)
---
 .../msq/dart/controller/DartControllerContext.java |  30 +++++-
 .../DartControllerContextFactoryImpl.java          |  13 ++-
 .../msq/dart/controller/DartMessageRelays.java     |  10 +-
 .../druid/msq/dart/guice/DartWorkerModule.java     |  17 ++++
 .../DartWorkerService.java}                        |  36 ++++---
 .../apache/druid/msq/guice/MSQIndexingModule.java  |   5 +
 .../dart/controller/DartControllerContextTest.java | 106 ++++++++++++++++++---
 .../msq/dart/worker/DartWorkerServiceTest.java     |  65 +++++++++++++
 .../test/TestDartControllerContextFactoryImpl.java |  74 +++++++++++++-
 9 files changed, 323 insertions(+), 33 deletions(-)

diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContext.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContext.java
index ec748a74b7d..cd8faecf09b 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContext.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContext.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.dart.controller;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.inject.Injector;
 import org.apache.druid.client.TimelineServerView;
+import org.apache.druid.discovery.DruidNodeDiscovery;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.indexing.common.TaskLockType;
 import org.apache.druid.indexing.common.actions.TaskActionClient;
@@ -52,6 +53,7 @@ import org.apache.druid.server.coordination.ServerType;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -91,6 +93,7 @@ public class DartControllerContext implements 
ControllerContext
   private final List<InputSpecSlicerProvider> inputSpecSlicerProviders;
   private final ServiceEmitter emitter;
   private final QueryContext context;
+  private final DruidNodeDiscovery dartWorkerDiscovery;
 
   public DartControllerContext(
       final Injector injector,
@@ -101,7 +104,8 @@ public class DartControllerContext implements 
ControllerContext
       final TimelineServerView serverView,
       final List<InputSpecSlicerProvider> inputSpecSlicerProviders,
       final ServiceEmitter emitter,
-      final QueryContext context
+      final QueryContext context,
+      final DruidNodeDiscovery dartWorkerDiscovery
   )
   {
     this.injector = injector;
@@ -113,6 +117,7 @@ public class DartControllerContext implements 
ControllerContext
     this.inputSpecSlicerProviders = inputSpecSlicerProviders;
     this.emitter = emitter;
     this.context = context;
+    this.dartWorkerDiscovery = dartWorkerDiscovery;
   }
 
   @Override
@@ -126,17 +131,38 @@ public class DartControllerContext implements 
ControllerContext
   {
     final List<DruidServerMetadata> servers = 
serverView.getDruidServerMetadatas();
 
+    final Set<String> dartWorkerHosts =
+        dartWorkerDiscovery.getAllNodes()
+                           .stream()
+                           .map(node -> 
node.getDruidNode().getHostAndPortToUse())
+                           .collect(Collectors.toSet());
+
     // Lock in the list of workers when creating the kernel config. There is a 
race here: the serverView itself is
     // allowed to float. If a segment moves to a new server that isn't part of 
our list after the WorkerManager is
     // created, we won't be able to find a valid server for certain segments. 
This isn't expected to be a problem,
     // since the serverView is referenced shortly after the worker list is 
created.
     final List<String> workerIds = new ArrayList<>(servers.size());
     for (final DruidServerMetadata server : servers) {
-      if (server.getType() == ServerType.HISTORICAL) {
+      if (server.getType() == ServerType.HISTORICAL && 
dartWorkerHosts.contains(server.getHost())) {
         workerIds.add(WorkerId.fromDruidServerMetadata(server, 
queryId()).toString());
       }
     }
 
+    // Fail fast rather than running a query with no workers.
+    if (workerIds.isEmpty()) {
+      final boolean anyHistoricals = servers.stream().anyMatch(s -> 
s.getType() == ServerType.HISTORICAL);
+      throw DruidException.forPersona(DruidException.Persona.OPERATOR)
+                          .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                          .build(
+                              anyHistoricals
+                              ? "No Dart workers are available: Historicals 
are present but none advertise a Dart "
+                                + "worker. Set druid.msq.dart.enabled=true on 
the Historicals that should run Dart "
+                                + "queries."
+                              : "No Dart workers are available: no Historicals 
are currently available to run the "
+                                + "query."
+                          );
+    }
+
     // Shuffle workerIds, so we don't bias towards specific servers when 
running multiple queries concurrently. For any
     // given query, lower-numbered workers tend to do more work, because the 
controller prefers using lower-numbered
     // workers when maxWorkerCount for a stage is less than the total number 
of workers.
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContextFactoryImpl.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContextFactoryImpl.java
index 260d6156c8c..a5839d20b58 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContextFactoryImpl.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartControllerContextFactoryImpl.java
@@ -23,6 +23,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import org.apache.druid.client.TimelineServerView;
+import org.apache.druid.discovery.DruidNodeDiscovery;
+import org.apache.druid.discovery.DruidNodeDiscoveryProvider;
+import org.apache.druid.discovery.NodeRole;
 import org.apache.druid.guice.annotations.EscalatedGlobal;
 import org.apache.druid.guice.annotations.Json;
 import org.apache.druid.guice.annotations.Self;
@@ -30,6 +33,7 @@ import org.apache.druid.guice.annotations.Smile;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.msq.dart.Dart;
 import org.apache.druid.msq.dart.worker.DartWorkerClientImpl;
+import org.apache.druid.msq.dart.worker.DartWorkerService;
 import org.apache.druid.msq.exec.ControllerContext;
 import org.apache.druid.msq.exec.MemoryIntrospector;
 import org.apache.druid.msq.input.InputSpecSlicerProvider;
@@ -52,6 +56,7 @@ public class DartControllerContextFactoryImpl implements 
DartControllerContextFa
   protected final MemoryIntrospector memoryIntrospector;
   protected final List<InputSpecSlicerProvider> inputSpecSlicerProviders;
   protected final ServiceEmitter emitter;
+  protected final DruidNodeDiscovery dartWorkerDiscovery;
 
   @Inject
   public DartControllerContextFactoryImpl(
@@ -63,7 +68,8 @@ public class DartControllerContextFactoryImpl implements 
DartControllerContextFa
       final MemoryIntrospector memoryIntrospector,
       final TimelineServerView serverView,
       @Dart final Set<InputSpecSlicerProvider> inputSpecSlicerProviders,
-      final ServiceEmitter emitter
+      final ServiceEmitter emitter,
+      final DruidNodeDiscoveryProvider discoveryProvider
   )
   {
     this.injector = injector;
@@ -75,6 +81,8 @@ public class DartControllerContextFactoryImpl implements 
DartControllerContextFa
     this.memoryIntrospector = memoryIntrospector;
     this.inputSpecSlicerProviders = List.copyOf(inputSpecSlicerProviders);
     this.emitter = emitter;
+    this.dartWorkerDiscovery =
+        discoveryProvider.getForServiceAndRoles(DartWorkerService.NAME, 
Set.of(NodeRole.HISTORICAL));
   }
 
   @Override
@@ -90,7 +98,8 @@ public class DartControllerContextFactoryImpl implements 
DartControllerContextFa
         serverView,
         inputSpecSlicerProviders,
         emitter,
-        context
+        context,
+        dartWorkerDiscovery
     );
   }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
index 23accd35ecb..0f1ec4b5eaf 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
@@ -24,6 +24,9 @@ import org.apache.druid.discovery.NodeRole;
 import org.apache.druid.messages.client.MessageRelayFactory;
 import org.apache.druid.messages.client.MessageRelays;
 import org.apache.druid.msq.dart.controller.messages.ControllerMessage;
+import org.apache.druid.msq.dart.worker.DartWorkerService;
+
+import java.util.Set;
 
 /**
  * Specialized {@link MessageRelays} for Dart controllers.
@@ -35,6 +38,11 @@ public class DartMessageRelays extends 
MessageRelays<ControllerMessage>
       final MessageRelayFactory<ControllerMessage> messageRelayFactory
   )
   {
-    super(() -> discoveryProvider.getForNodeRole(NodeRole.HISTORICAL), 
messageRelayFactory);
+    // Only relay with Historicals that run a Dart worker (advertise 
DartWorkerService); Dart-disabled Historicals
+    // expose no outbox. This is the same discovery handle the controller uses 
to enroll workers.
+    super(
+        () -> discoveryProvider.getForServiceAndRoles(DartWorkerService.NAME, 
Set.of(NodeRole.HISTORICAL)),
+        messageRelayFactory
+    );
   }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartWorkerModule.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartWorkerModule.java
index 406c2acac84..dd220c4d7e2 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartWorkerModule.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartWorkerModule.java
@@ -28,7 +28,10 @@ import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provides;
+import com.google.inject.multibindings.ProvidesIntoSet;
+import com.google.inject.name.Named;
 import org.apache.druid.discovery.DruidNodeDiscoveryProvider;
+import org.apache.druid.discovery.DruidService;
 import org.apache.druid.discovery.NodeRole;
 import org.apache.druid.guice.Jerseys;
 import org.apache.druid.guice.JsonConfigProvider;
@@ -57,6 +60,7 @@ import 
org.apache.druid.msq.dart.worker.DartSegmentsInputSliceReaderProvider;
 import org.apache.druid.msq.dart.worker.DartWorkerContextFactory;
 import org.apache.druid.msq.dart.worker.DartWorkerContextFactoryImpl;
 import org.apache.druid.msq.dart.worker.DartWorkerRunner;
+import org.apache.druid.msq.dart.worker.DartWorkerService;
 import org.apache.druid.msq.dart.worker.http.DartWorkerResource;
 import org.apache.druid.msq.exec.MemoryIntrospector;
 import org.apache.druid.msq.guice.MSQBinders;
@@ -118,6 +122,19 @@ public class DartWorkerModule implements DruidModule
                 .in(LazySingleton.class);
     }
 
+    /**
+     * Advertise {@link DartWorkerService} in node discovery. Contributed from 
{@link ActualModule}, which is only
+     * installed when Dart is enabled, so advertisement tracks actually 
running a Dart worker. Merges into the same
+     * {@code @Named("historical")} service set announced by
+     * {@link org.apache.druid.guice.HistoricalServiceModule}.
+     */
+    @ProvidesIntoSet
+    @Named(NodeRole.HISTORICAL_JSON_NAME)
+    public Class<? extends DruidService> getDartWorkerService()
+    {
+      return DartWorkerService.class;
+    }
+
     @Provides
     @ManageLifecycle
     public DartWorkerRunner createWorkerRunner(
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartWorkerService.java
similarity index 53%
copy from 
multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
copy to 
multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartWorkerService.java
index 23accd35ecb..910ce23ef85 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/DartMessageRelays.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartWorkerService.java
@@ -17,24 +17,34 @@
  * under the License.
  */
 
-package org.apache.druid.msq.dart.controller;
+package org.apache.druid.msq.dart.worker;
 
-import org.apache.druid.discovery.DruidNodeDiscoveryProvider;
-import org.apache.druid.discovery.NodeRole;
-import org.apache.druid.messages.client.MessageRelayFactory;
-import org.apache.druid.messages.client.MessageRelays;
-import org.apache.druid.msq.dart.controller.messages.ControllerMessage;
+import org.apache.druid.discovery.DruidService;
 
 /**
- * Specialized {@link MessageRelays} for Dart controllers.
+ * No-payload {@link DruidService} advertised in node discovery by a 
Historical whose Dart worker runtime is
+ * installed (i.e. {@code druid.msq.dart.enabled=true}). The Dart controller 
enrolls workers only when they are
+ * Historicals that advertise this capability.
  */
-public class DartMessageRelays extends MessageRelays<ControllerMessage>
+public class DartWorkerService extends DruidService
 {
-  public DartMessageRelays(
-      final DruidNodeDiscoveryProvider discoveryProvider,
-      final MessageRelayFactory<ControllerMessage> messageRelayFactory
-  )
+  public static final String NAME = "dartWorkerService";
+
+  @Override
+  public String getName()
+  {
+    return NAME;
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    return o != null && getClass() == o.getClass();
+  }
+
+  @Override
+  public int hashCode()
   {
-    super(() -> discoveryProvider.getForNodeRole(NodeRole.HISTORICAL), 
messageRelayFactory);
+    return DartWorkerService.class.hashCode();
   }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
index 8e472e6e7d8..a18b7cddfcb 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
@@ -40,6 +40,7 @@ import 
org.apache.druid.msq.counters.SegmentGenerationProgressCounter;
 import org.apache.druid.msq.counters.StorageCounters;
 import org.apache.druid.msq.counters.SuperSorterProgressTrackerCounter;
 import org.apache.druid.msq.counters.WarningCounters;
+import org.apache.druid.msq.dart.worker.DartWorkerService;
 import org.apache.druid.msq.indexing.IndexerControllerContextFactory;
 import org.apache.druid.msq.indexing.IndexerSegmentsInputSliceReaderProvider;
 import org.apache.druid.msq.indexing.IndexerTableInputSpecSlicerProvider;
@@ -229,6 +230,10 @@ public class MSQIndexingModule implements DruidModule
 
     module.registerSubtypes(new NamedType(MSQCompactionRunner.class, 
MSQCompactionRunner.TYPE));
 
+    // Registered here rather than in DartWorkerModule so that every process 
that discovers Historicals can parse
+    // their DartWorkerService announcement.
+    module.registerSubtypes(new NamedType(DartWorkerService.class, 
DartWorkerService.NAME));
+
     FAULT_CLASSES.forEach(module::registerSubtypes);
     module.addSerializer(new CounterSnapshotsSerializer());
     return Collections.singletonList(module);
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/DartControllerContextTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/DartControllerContextTest.java
index 1ab1b55024c..d21e181bcd7 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/DartControllerContextTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/DartControllerContextTest.java
@@ -22,12 +22,17 @@ package org.apache.druid.msq.dart.controller;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import org.apache.druid.client.BrokerServerView;
+import org.apache.druid.discovery.DiscoveryDruidNode;
+import org.apache.druid.discovery.DruidNodeDiscovery;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.msq.dart.worker.DartWorkerService;
 import org.apache.druid.msq.dart.worker.WorkerId;
 import org.apache.druid.msq.exec.MemoryIntrospector;
 import org.apache.druid.msq.exec.MemoryIntrospectorImpl;
 import org.apache.druid.msq.indexing.LegacyMSQSpec;
 import org.apache.druid.msq.indexing.destination.TaskReportMSQDestination;
 import org.apache.druid.msq.kernel.controller.ControllerQueryKernelConfig;
+import org.apache.druid.msq.test.TestDartControllerContextFactoryImpl;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryContext;
@@ -43,6 +48,7 @@ import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -104,9 +110,68 @@ public class DartControllerContextTest
   }
 
   @Test
-  public void test_queryKernelConfig()
+  public void test_queryKernelConfig_allHistoricalsAdvertiseDartWorker()
   {
-    final DartControllerContext controllerContext = new DartControllerContext(
+    // Both HISTORICALs advertise a Dart worker: enroll both, matching 
pre-capability behavior.
+    final ControllerQueryKernelConfig queryKernelConfig =
+        makeControllerContext(discoveryOf(SERVERS.get(0), 
SERVERS.get(1))).queryKernelConfig(querySpec);
+
+    assertCommonKernelConfig(queryKernelConfig);
+    Assertions.assertEquals(
+        ImmutableList.of(
+            WorkerId.fromDruidServerMetadata(SERVERS.get(0), 
QUERY_ID).toString(),
+            WorkerId.fromDruidServerMetadata(SERVERS.get(1), 
QUERY_ID).toString()
+        ),
+        sortedWorkerIds(queryKernelConfig)
+    );
+  }
+
+  @Test
+  public void test_queryKernelConfig_onlySomeHistoricalsAdvertiseDartWorker()
+  {
+    // Only the first HISTORICAL advertises a Dart worker: the Dart-disabled 
one is excluded.
+    final ControllerQueryKernelConfig queryKernelConfig =
+        
makeControllerContext(discoveryOf(SERVERS.get(0))).queryKernelConfig(querySpec);
+
+    assertCommonKernelConfig(queryKernelConfig);
+    Assertions.assertEquals(
+        ImmutableList.of(WorkerId.fromDruidServerMetadata(SERVERS.get(0), 
QUERY_ID).toString()),
+        sortedWorkerIds(queryKernelConfig)
+    );
+  }
+
+  @Test
+  public void 
test_queryKernelConfig_noHistoricalsAdvertiseDartWorker_failsFast()
+  {
+    // Historicals are present in the server view but none advertise a Dart 
worker so we must fail fast
+    final DruidException e = Assertions.assertThrows(
+        DruidException.class,
+        () -> makeControllerContext(discoveryOf()).queryKernelConfig(querySpec)
+    );
+
+    Assertions.assertEquals(DruidException.Persona.OPERATOR, 
e.getTargetPersona());
+    Assertions.assertTrue(e.getMessage().contains("No Dart workers are 
available"), e.getMessage());
+    Assertions.assertTrue(e.getMessage().contains("druid.msq.dart.enabled"), 
e.getMessage());
+  }
+
+  @Test
+  public void test_queryKernelConfig_noHistoricalsAtAll_failsFast()
+  {
+    // No historical servers at all: still fail fast, but with a descriptive 
message about the cause
+    Mockito.when(serverView.getDruidServerMetadatas())
+           .thenReturn(ImmutableList.of(SERVERS.get(2))); // realtime only
+
+    final DruidException e = Assertions.assertThrows(
+        DruidException.class,
+        () -> makeControllerContext(discoveryOf()).queryKernelConfig(querySpec)
+    );
+
+    Assertions.assertTrue(e.getMessage().contains("no Historicals are 
currently available"), e.getMessage());
+  }
+
+  private DartControllerContext makeControllerContext(final DruidNodeDiscovery 
dartWorkerDiscovery)
+  {
+    return new DartControllerContext(
         null,
         null,
         SELF_NODE,
@@ -115,24 +180,39 @@ public class DartControllerContextTest
         serverView,
         List.of(),
         null,
-        queryContext
+        queryContext,
+        dartWorkerDiscovery
     );
-    final ControllerQueryKernelConfig queryKernelConfig = 
controllerContext.queryKernelConfig(querySpec);
+  }
 
+  private static void assertCommonKernelConfig(final 
ControllerQueryKernelConfig queryKernelConfig)
+  {
     Assertions.assertFalse(queryKernelConfig.isFaultTolerant());
     Assertions.assertFalse(queryKernelConfig.isDurableStorage());
     Assertions.assertEquals(3, queryKernelConfig.getMaxConcurrentStages());
     Assertions.assertEquals(TaskReportMSQDestination.instance(), 
queryKernelConfig.getDestination());
     Assertions.assertTrue(queryKernelConfig.isPipeline());
+  }
 
-    // Check workerIds after sorting, because they've been shuffled.
-    Assertions.assertEquals(
-        ImmutableList.of(
-            // Only the HISTORICAL servers
-            WorkerId.fromDruidServerMetadata(SERVERS.get(0), 
QUERY_ID).toString(),
-            WorkerId.fromDruidServerMetadata(SERVERS.get(1), 
QUERY_ID).toString()
-        ),
-        
queryKernelConfig.getWorkerIds().stream().sorted().collect(Collectors.toList())
-    );
+  /**
+   * The workerIds are shuffled by {@link 
DartControllerContext#queryKernelConfig}, so sort before comparing.
+   */
+  private static List<String> sortedWorkerIds(final 
ControllerQueryKernelConfig queryKernelConfig)
+  {
+    return 
queryKernelConfig.getWorkerIds().stream().sorted().collect(Collectors.toList());
+  }
+
+  /**
+   * A {@link DruidNodeDiscovery} that reports the given servers as 
Historicals advertising a {@link DartWorkerService}.
+   */
+  private static DruidNodeDiscovery discoveryOf(final DruidServerMetadata... 
servers)
+  {
+    final List<DiscoveryDruidNode> nodes =
+        Arrays.stream(servers)
+              
.map(TestDartControllerContextFactoryImpl::historicalDartWorkerNode)
+              .collect(Collectors.toList());
+    final DruidNodeDiscovery discovery = 
Mockito.mock(DruidNodeDiscovery.class);
+    Mockito.when(discovery.getAllNodes()).thenReturn(nodes);
+    return discovery;
   }
 }
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/worker/DartWorkerServiceTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/worker/DartWorkerServiceTest.java
new file mode 100644
index 00000000000..88f5bf2873c
--- /dev/null
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/worker/DartWorkerServiceTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.msq.dart.worker;
+
+import com.fasterxml.jackson.databind.InjectableValues;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import org.apache.druid.discovery.DiscoveryDruidNode;
+import org.apache.druid.discovery.DruidService;
+import org.apache.druid.discovery.NodeRole;
+import org.apache.druid.guice.ServerModule;
+import org.apache.druid.jackson.DefaultObjectMapper;
+import org.apache.druid.msq.guice.MSQIndexingModule;
+import org.apache.druid.server.DruidNode;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies a Historical's {@link DartWorkerService} announcement round-trips 
through the {@link DruidService}
+ * polymorphic types, so any process with {@link MSQIndexingModule} installed 
can read it and discover the node.
+ */
+public class DartWorkerServiceTest
+{
+  private static final DruidNode HISTORICAL_NODE =
+      new DruidNode("no", "localhost", false, 8083, -1, true, false);
+
+  @Test
+  public void test_serde_withinDiscoveryDruidNode() throws Exception
+  {
+    final ObjectMapper mapper = createMapper();
+    final DiscoveryDruidNode node = new DiscoveryDruidNode(
+        HISTORICAL_NODE,
+        NodeRole.HISTORICAL,
+        ImmutableMap.of(DartWorkerService.NAME, new DartWorkerService())
+    );
+    final String json = mapper.writeValueAsString(node);
+    Assertions.assertEquals(node, mapper.readValue(json, 
DiscoveryDruidNode.class));
+  }
+
+  private static ObjectMapper createMapper()
+  {
+    final ObjectMapper mapper = new DefaultObjectMapper();
+    mapper.registerModules(new ServerModule().getJacksonModules());
+    mapper.registerModules(new MSQIndexingModule().getJacksonModules());
+    mapper.setInjectableValues(new 
InjectableValues.Std().addValue(ObjectMapper.class, mapper));
+    return mapper;
+  }
+}
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/test/TestDartControllerContextFactoryImpl.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/test/TestDartControllerContextFactoryImpl.java
index d7f48f2dc41..3dd81e204d4 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/test/TestDartControllerContextFactoryImpl.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/test/TestDartControllerContextFactoryImpl.java
@@ -20,12 +20,18 @@
 package org.apache.druid.msq.test;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.net.HostAndPort;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import org.apache.druid.client.TimelineServerView;
+import org.apache.druid.discovery.DiscoveryDruidNode;
+import org.apache.druid.discovery.DruidNodeDiscovery;
+import org.apache.druid.discovery.DruidNodeDiscoveryProvider;
+import org.apache.druid.discovery.NodeRole;
 import org.apache.druid.guice.annotations.EscalatedGlobal;
 import org.apache.druid.guice.annotations.Json;
 import org.apache.druid.guice.annotations.Self;
@@ -38,6 +44,7 @@ import org.apache.druid.msq.dart.Dart;
 import org.apache.druid.msq.dart.controller.DartControllerContext;
 import org.apache.druid.msq.dart.controller.DartControllerContextFactoryImpl;
 import org.apache.druid.msq.dart.worker.DartWorkerClient;
+import org.apache.druid.msq.dart.worker.DartWorkerService;
 import org.apache.druid.msq.exec.Controller;
 import org.apache.druid.msq.exec.ControllerContext;
 import org.apache.druid.msq.exec.MSQMetricEventBuilder;
@@ -52,10 +59,15 @@ import org.apache.druid.msq.kernel.WorkOrder;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.rpc.ServiceClientFactory;
 import org.apache.druid.server.DruidNode;
+import org.apache.druid.server.coordination.DruidServerMetadata;
+import org.apache.druid.server.coordination.ServerType;
 
+import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
 
 public class TestDartControllerContextFactoryImpl extends 
DartControllerContextFactoryImpl
 {
@@ -78,6 +90,7 @@ public class TestDartControllerContextFactoryImpl extends 
DartControllerContextF
       final TimelineServerView serverView,
       @Dart final Set<InputSpecSlicerProvider> inputSpecSlicerProviders,
       final ServiceEmitter emitter,
+      final DruidNodeDiscoveryProvider discoveryProvider,
       @Dart Map<String, WorkerRunRef> workerMap
   )
   {
@@ -90,7 +103,8 @@ public class TestDartControllerContextFactoryImpl extends 
DartControllerContextF
         memoryIntrospector,
         serverView,
         inputSpecSlicerProviders,
-        emitter
+        emitter,
+        discoveryProvider
     );
     this.workerMap = workerMap;
   }
@@ -107,7 +121,8 @@ public class TestDartControllerContextFactoryImpl extends 
DartControllerContextF
         serverView,
         inputSpecSlicerProviders,
         emitter,
-        context
+        context,
+        advertiseAllHistoricals(serverView)
     )
     {
       @Override
@@ -131,6 +146,61 @@ public class TestDartControllerContextFactoryImpl extends 
DartControllerContextF
     };
   }
 
+  /**
+   * Builds a {@link DruidNodeDiscovery} that reports every {@link 
ServerType#HISTORICAL} in the server view as a
+   * Dart worker (advertising {@link DartWorkerService}). {@link 
DartControllerContext#queryKernelConfig} reads
+   * only {@link DruidNodeDiscovery#getAllNodes()}, so a no-op listener 
registration is sufficient.
+   */
+  private static DruidNodeDiscovery advertiseAllHistoricals(final 
TimelineServerView serverView)
+  {
+    final List<DiscoveryDruidNode> nodes =
+        serverView.getDruidServerMetadatas()
+                  .stream()
+                  .filter(server -> server.getType() == ServerType.HISTORICAL)
+                  
.map(TestDartControllerContextFactoryImpl::historicalDartWorkerNode)
+                  .collect(Collectors.toList());
+
+    return new DruidNodeDiscovery()
+    {
+      @Override
+      public Collection<DiscoveryDruidNode> getAllNodes()
+      {
+        return nodes;
+      }
+
+      @Override
+      public void registerListener(Listener listener)
+      {
+        // Not used by queryKernelConfig.
+      }
+    };
+  }
+
+  /**
+   * A Historical {@link DiscoveryDruidNode} advertising {@link 
DartWorkerService}, whose
+   * {@link DruidNode#getHostAndPortToUse()} matches {@link 
DruidServerMetadata#getHost()}.
+   */
+  public static DiscoveryDruidNode historicalDartWorkerNode(final 
DruidServerMetadata server)
+  {
+    // Build a DruidNode whose getHostAndPortToUse() equals server.getHost() 
(the key queryKernelConfig matches on).
+    final boolean tls = server.getHostAndTlsPort() != null;
+    final HostAndPort hostAndPort = HostAndPort.fromString(server.getHost());
+    final DruidNode druidNode = new DruidNode(
+        "no",
+        hostAndPort.getHost(),
+        false,
+        tls ? -1 : hostAndPort.getPort(),
+        tls ? hostAndPort.getPort() : -1,
+        !tls,
+        tls
+    );
+    return new DiscoveryDruidNode(
+        druidNode,
+        NodeRole.HISTORICAL,
+        ImmutableMap.of(DartWorkerService.NAME, new DartWorkerService())
+    );
+  }
+
   public class DartTestWorkerClient extends MSQTestWorkerClient implements 
DartWorkerClient
   {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to