Skip to content

Conversation

@abhishekrb19
Copy link
Contributor

@abhishekrb19 abhishekrb19 commented Nov 10, 2025

Refactor:

  • Remove named injections of DataSourceTaskIdHolder in favor of:
    • TaskHolder interface: implemented by PeonTaskHolder for peon processes and by NoopTaskHolder for all other servers, where the methods return null. The alternative was to throw UnsupportedExceptions in NoopTaskHolder and have callers verify type, but that made the usages a bit difficult and the callers would still have to effectively assign null anyway.
    • LoadSpecHolder interface: implemented by PeonLoadSpecHolder for peon processes and DefaultLoadSpecHolder for all other servers.
  • A module DefaultServerHolderModule that binds the above modules as part of the core injector module for all servers except CliPeon. For CliPeon, the binding to PeonTaskHolder and PeonLoadSpecHolder is explicitly done in CliPeon itself.

Some implementation specific choices have been documented as javadocs in the code. In the future, we can extend TaskHolder to also include groupId, taskType, etc., from the Task and add those dimensions to all the metric modules as applicable.

This PR has:

  • been self-reviewed.
  • a release note entry in the PR description.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • been tested in a test Druid cluster.

@abhishekrb19 abhishekrb19 requested a review from kfaraz November 10, 2025 05:07
@github-actions github-actions bot added the GHA label Nov 10, 2025
Copy link
Contributor

@kfaraz kfaraz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together, @abhishekrb19 !

I had a slightly different approach in mind.

In the original class, DatasourceTaskIdHolder all the fields are derived from Task itself, so we should probably just:

  • Rename it to PeonTaskHolder to denote that this class is meaningful only for a peon and that it contains the task that the peon is running
  • Just keep one field inside it Task task and add method getTask()
  • Retain the original utility methods getDatasource(), getTaskId(), getLookupLoadinSpec() etc.
  • Create the PeonTaskHolder in a @Provides method in CliPeon.
  • In non peon servers, bind the PeonTaskHolder to null.

Let me know if this would make sense.

@abhishekrb19
Copy link
Contributor Author

abhishekrb19 commented Nov 10, 2025

Thanks for the review, @kfaraz!

I think the approach of keeping one renamed class would work well and makes sense. Just one caveat: the Task class is in the indexing-service module, so we can’t use it directly in the server module. However, we could create a constructor that accepts all the task properties of interest and have it @Provides from CliPeon like you suggested, along with a default constructor for all other servers. This way, we can remove all the individual @Named annotation bindings from CliPeon to DatasourceTaskIdHolder. Does that work?

@kfaraz
Copy link
Contributor

kfaraz commented Nov 10, 2025

Ah, I see, thanks for clarifying that, @abhishekrb19 ! All the current usages of DatasourceTaskIdHolder are indeed in the druid-server module.

In that case, I think we should add an interface TaskHolder to druid-server and use it wherever needed.
Bind the TaskHolder to a PeonTaskHolder in CliPeon and bind it elsewhere to a NoopTaskHolder which throws UnsupportedOperationException upon calling any method. I feel this clarifies the intent better and makes it more extensible for the future.

Sound good?

@abhishekrb19
Copy link
Contributor Author

@kfaraz sorry I ran into some hiccups with the bindings last week and didn't get a chance to take a closer look, but I'm hoping to revive this change again soon.

Btw I noticed this deprecated comment on EmbeddedMiddleManager, so most (or all?) the tests seem to use the indexer for good reasons. Given that there are some subtle differences between indexers and MMs, do you have any suggestions for embedded tests that use MMs?

At the very least, I was thinking of updating some of the embedded tests to use EmbeddedMiddleManager instead of EmbeddedIndexer locally. I’m also planning to add some unit tests.

@kfaraz
Copy link
Contributor

kfaraz commented Nov 20, 2025

@kfaraz sorry I ran into some hiccups with the bindings last week and didn't get a chance to take a closer look, but I'm hoping to revive this change again soon.

No worries, @abhishekrb19 ! 🙂

Given that there are some subtle differences between indexers and MMs, do you have any suggestions for embedded tests that use MMs?

If the purpose is to test out peons in an embedded test setup, you could do a couple of things:

  • Use EmbeddedMiddleManager and annotate it @SuppressForbidden(reason = "EmbeddedMiddleManager#init")
  • Use the k3s environment with the help of K3sClusterResource, e.g. KubernetesClusterDockerTest (These tests would run in the docker-tests GHA workflow. It is also possible to run them locally after setting the druid.testing.docker.image property.)

For local testing, please feel free to update any test to use an MM instead of Indexer. Most of them should work fine for both Indexer and MM.
But while committing, I would advise not to change any of the existing tests to use MMs (since Indexers are faster and easier to debug) and add new MM-flavored variants of any test where necessary.

Please let me know if this meets yours needs.

@abhishekrb19
Copy link
Contributor Author

Sounds good, thanks for the pointer @kfaraz! I will give that a try

@abhishekrb19 abhishekrb19 force-pushed the refactor_ds_lookup_holder branch from 98ff33e to 6fd60d6 Compare November 24, 2025 23:52
"Cannot specify both `lookupTier` and `lookupTierIsDatasource`"
);
final String lookupTier = lookupTierIsDatasource ? dataSourceTaskIdHolder.getDataSource() : this.lookupTier;
final String lookupTier = lookupTierIsDatasource ? taskHolder.getDataSource() : this.lookupTier;

Check notice

Code scanning / CodeQL

Possible confusion of local and field Note

Confusing name: method
getLookupTier
also refers to field
lookupTier
(without qualifying it with 'this').

Copilot Autofix

AI about 1 month ago

The best fix is to rename the local variable lookupTier in the getLookupTier() method to a different name, such as tierToReturn or effectiveLookupTier. This removes any ambiguity between the field and the local variable. This change should only be performed in the method getLookupTier() within LookupListeningAnnouncerConfig.java, specifically where the local variable is declared and used. No imports or additional definitions are necessary, as it's a straightforward renaming.


Suggested changeset 1
server/src/main/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfig.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/src/main/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfig.java b/server/src/main/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfig.java
--- a/server/src/main/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfig.java
+++ b/server/src/main/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfig.java
@@ -56,10 +56,10 @@
         "Cannot specify both `lookupTier` and `lookupTierIsDatasource`"
     );
 
-    final String lookupTier = lookupTierIsDatasource ? taskHolder.getDataSource() : this.lookupTier;
+    final String effectiveLookupTier = lookupTierIsDatasource ? taskHolder.getDataSource() : this.lookupTier;
 
     return Preconditions.checkNotNull(
-        lookupTier == null ? DEFAULT_TIER : StringUtils.emptyToNullNonDruidDataString(lookupTier),
+        effectiveLookupTier == null ? DEFAULT_TIER : StringUtils.emptyToNullNonDruidDataString(effectiveLookupTier),
         "Cannot have empty lookup tier from %s",
         lookupTierIsDatasource ? "bound value" : LookupModule.PROPERTY_BASE
     );
EOF
@@ -56,10 +56,10 @@
"Cannot specify both `lookupTier` and `lookupTierIsDatasource`"
);

final String lookupTier = lookupTierIsDatasource ? taskHolder.getDataSource() : this.lookupTier;
final String effectiveLookupTier = lookupTierIsDatasource ? taskHolder.getDataSource() : this.lookupTier;

return Preconditions.checkNotNull(
lookupTier == null ? DEFAULT_TIER : StringUtils.emptyToNullNonDruidDataString(lookupTier),
effectiveLookupTier == null ? DEFAULT_TIER : StringUtils.emptyToNullNonDruidDataString(effectiveLookupTier),
"Cannot have empty lookup tier from %s",
lookupTierIsDatasource ? "bound value" : LookupModule.PROPERTY_BASE
);
Copilot is powered by AI and may make mistakes. Always verify output.
@abhishekrb19 abhishekrb19 force-pushed the refactor_ds_lookup_holder branch 2 times, most recently from 9faa21f to 9513f0c Compare November 25, 2025 01:42
@abhishekrb19 abhishekrb19 force-pushed the refactor_ds_lookup_holder branch from 9513f0c to 89e3418 Compare November 25, 2025 01:47
Comment on lines +186 to +195
final Injector injector = Initialization.makeInjectorWithModules(
GuiceInjectors.makeStartupInjector(),
ImmutableList.of(
(Module) binder -> JsonConfigProvider.bindInstance(
binder,
Key.get(DruidNode.class, Self.class),
new DruidNode("test-inject", null, false, null, null, true, false)
),
new LookupModule()
));

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
Initialization.makeInjectorWithModules
should be avoided because it has been deprecated.

Copilot Autofix

AI about 1 month ago

To address the issue, we need to replace the usage of the deprecated Initialization.makeInjectorWithModules method (line 174) with its recommended alternative. According to the Druid source code and upgrade notes, the recommended replacement is generally the static utility method GuiceInjectors.makeInjectorWithModules, which provides the same API for creating an Injector from an existing startup injector and extra modules.

Specifically, you should:

  • Replace the invocation of Initialization.makeInjectorWithModules(...) with GuiceInjectors.makeInjectorWithModules(...) in the test method testLookupTierDefaultsForNonPeonServers.
  • No changes to method parameters should be needed, as both methods accept the same arguments.
  • All necessary imports are already present (GuiceInjectors is imported at line 27).
  • No further changes are required elsewhere in the provided snippet.

Suggested changeset 1
server/src/test/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfigTest.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/src/test/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfigTest.java b/server/src/test/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfigTest.java
--- a/server/src/test/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfigTest.java
+++ b/server/src/test/java/org/apache/druid/query/lookup/LookupListeningAnnouncerConfigTest.java
@@ -171,7 +171,7 @@
   @Test
   public void testLookupTierDefaultsForNonPeonServers()
   {
-    final Injector injector = Initialization.makeInjectorWithModules(
+    final Injector injector = GuiceInjectors.makeInjectorWithModules(
         GuiceInjectors.makeStartupInjector(),
         ImmutableList.of(
             (Module) binder -> JsonConfigProvider.bindInstance(
EOF
@@ -171,7 +171,7 @@
@Test
public void testLookupTierDefaultsForNonPeonServers()
{
final Injector injector = Initialization.makeInjectorWithModules(
final Injector injector = GuiceInjectors.makeInjectorWithModules(
GuiceInjectors.makeStartupInjector(),
ImmutableList.of(
(Module) binder -> JsonConfigProvider.bindInstance(
Copilot is powered by AI and may make mistakes. Always verify output.
@abhishekrb19 abhishekrb19 requested a review from kfaraz November 27, 2025 05:44
Copy link
Contributor

@kfaraz kfaraz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some final suggestions, otherwise looks good. Thanks for incorporating the changes, @abhishekrb19 !

Comment on lines 111 to 112
// CliPeon.runTask and TaskHolder.getDataSource()/TaskHolder.getTaskId(). The reason for this is unclear
// but by injecting TaskPropertiesHolder early this cycle is avoided.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true even after the changes in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, good point, they're no longer required. I've removed the changes originally included in #16140 and added some tests that would otherwise fail if we remove this "hack".

@Nullable final String datasource,
@Nullable final String taskId
)
public static Map<String, String[]> mapOfTaskHolderDimensions(final TaskHolder taskHolder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would now make sense to move this method into TaskHolder itself. The map would be created just once when the TaskHolder instance is created.

Then we can just call taskHolder.getMetricDimensions() and get the same immutable wherever needed.

private Provider<Task> taskProvider;

@Inject
public PeonTaskHolder(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Do not use @Inject with this constructor and just pass a Task into it instead of a Provider.
Use an @Provides method in CliPeon to bind it correctly.

@Provides
public TaskHolder getTaskHolder(Task task)
{
   return new PeonTaskHolder(task);
}

Copy link
Contributor Author

@abhishekrb19 abhishekrb19 Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are cyclic dependencies involving Task when it contains a TransformSpec, an AggregatorFactory or a monitor, which will pull in LookupModule, MetricsModule, etc, which depend on these holders. I’ve expanded the Javadoc to explain this in more detail. To break the cycle, this binding needs to use a Provider
instead of direct injection. Switching to @Provides pattern will break UTs and some ITs with a cyclic dependency Guice instantiation error:

End of classname legend:
========================

	at org.apache.druid.cli.CliPeon.run(CliPeon.java:413)
	at org.apache.druid.cli.Main.main(Main.java:111)
Caused by: java.lang.RuntimeException: com.google.inject.CreationException: Unable to create injector, see the following errors:

1) [Guice/ErrorInCustomProvider]: RuntimeException: ValueInstantiationException: Cannot construct instance of `ExpressionTransform`, problem: Unable to provision, see the following errors:

1) Problem parsing object at prefix[druid.lookup]: Cannot construct instance of `LookupListeningAnnouncerConfig`, problem: Unable to provision, see the following errors:

1) [Guice/ErrorInCustomProvider]: IllegalStateException: This is a proxy used to support circular references.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for the clarification!

private Provider<Task> taskProvider;

@Inject
public PeonLoadSpecHolder(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to PeonTaskHolder, this constructor should not need @Inject. Use a @Provides method in CliPeon instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional - please see #18732 (comment)

Copy link
Contributor

@kfaraz kfaraz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only pending comment is moving the mapOfDimensions method to TaskHolder, but that is not a blocker for this PR.

}

@Test
public void testTaskWithMonitorsAndMetricsSpecDoNotCauseCyclicDependency() throws IOException
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding these tests!

@abhishekrb19
Copy link
Contributor Author

Thanks, @kfaraz!

@abhishekrb19 abhishekrb19 merged commit b3be269 into master Dec 1, 2025
111 of 113 checks passed
@abhishekrb19 abhishekrb19 deleted the refactor_ds_lookup_holder branch December 1, 2025 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants