Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.temporal.spring.boot.autoconfigure;

import io.temporal.spring.boot.autoconfigure.properties.WorkerProperties;
import io.temporal.spring.boot.autoconfigure.properties.WorkersAutoDiscoveryProperties;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
Expand All @@ -15,11 +16,10 @@ class WorkersPresentCondition extends SpringBootCondition {
private static final Bindable<List<WorkerProperties>> WORKER_PROPERTIES_LIST =
Bindable.listOf(WorkerProperties.class);

private static final Bindable<List<String>> AUTO_DISCOVERY_PACKAGES_LIST =
Bindable.listOf(String.class);
private static final Bindable<WorkersAutoDiscoveryProperties> AUTO_DISCOVERY_BINDABLE =
Bindable.of(WorkersAutoDiscoveryProperties.class);
private static final String WORKERS_KEY = "spring.temporal.workers";
private static final String AUTO_DISCOVERY_KEY =
"spring.temporal.workers-auto-discovery.packages";
private static final String AUTO_DISCOVERY_KEY = "spring.temporal.workers-auto-discovery";

public WorkersPresentCondition() {}

Expand All @@ -34,8 +34,8 @@ public ConditionOutcome getMatchOutcome(
}

BindResult<?> autoDiscoveryProperty =
Binder.get(context.getEnvironment()).bind(AUTO_DISCOVERY_KEY, AUTO_DISCOVERY_PACKAGES_LIST);
messageBuilder = ConditionMessage.forCondition("Auto Discovery Packages Set");
Binder.get(context.getEnvironment()).bind(AUTO_DISCOVERY_KEY, AUTO_DISCOVERY_BINDABLE);
messageBuilder = ConditionMessage.forCondition("Workers Auto Discovery Set");
if (autoDiscoveryProperty.isBound()) {
return ConditionOutcome.match(messageBuilder.found("property").items(AUTO_DISCOVERY_KEY));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,121 @@
package io.temporal.spring.boot.autoconfigure.properties;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;

public class WorkersAutoDiscoveryProperties {
private final @Nullable List<String> packages;
/**
* When {@code true}, enables auto-registration of all {@code @ActivityImpl}-,
* {@code @NexusServiceImpl}-, and {@code @WorkflowImpl}-annotated classes that are Spring-managed
* beans. This is the recommended option for simple use cases where all implementations are
* Spring-managed beans. It implies {@link #registerActivityBeans} and {@link
* #registerNexusServiceBeans} default to {@code true}, and also enables auto-registration of
* {@code @WorkflowImpl}-annotated classes that are Spring-managed beans, without needing {@link
* #workflowPackages}.
*
* <p>Classpath-scanning via {@link #workflowPackages} (for non-bean workflow classes) still
* requires explicit package configuration regardless of this flag.
*/
private final @Nullable Boolean enabled;

private final @Nullable List<String> workflowPackages;
private final @Nullable Boolean registerActivityBeans;
private final @Nullable Boolean registerNexusServiceBeans;

/**
* @deprecated Use {@link #workflowPackages} instead. If set and non-empty, this property causes
* {@link #registerActivityBeans} and {@link #registerNexusServiceBeans} to default to {@code
* true}, and its entries to be considered as if they were provided through {@link
* #workflowPackages}. Setting both {@link #packages} and any of the other new properties is
* unsupported and will result in an exception.
*/
@Deprecated private final @Nullable List<String> packages;

@ConstructorBinding
public WorkersAutoDiscoveryProperties(@Nullable List<String> packages) {
public WorkersAutoDiscoveryProperties(
@Nullable Boolean enabled,
@Nullable List<String> workflowPackages,
@Nullable Boolean registerActivityBeans,
@Nullable Boolean registerNexusServiceBeans,
@Nullable List<String> packages) {
if (packages != null
&& !packages.isEmpty()
&& (enabled != null
|| workflowPackages != null
|| registerActivityBeans != null
|| registerNexusServiceBeans != null)) {
throw new IllegalStateException(
"spring.temporal.workers-auto-discovery.packages is deprecated and cannot be combined "
+ "with enabled, workflow-packages, register-activity-beans, or register-nexus-service-beans. "
+ "Migrate to the new properties and remove packages.");
}
this.enabled = enabled;
this.workflowPackages = workflowPackages;
this.registerActivityBeans = registerActivityBeans;
this.registerNexusServiceBeans = registerNexusServiceBeans;
this.packages = packages;
}

/**
* Returns whether {@code @WorkflowImpl}-annotated classes that are also Spring-managed beans
* should be automatically registered with matching workers (without classpath scanning). Defaults
* to {@code true} when {@link #enabled} is {@code true}, {@code false} otherwise. Workflow
* implementation classes that are not Spring-managed beans still require {@link
* #workflowPackages} for classpath scanning.
*/
public boolean isRegisterWorkflowManagedBeans() {
return Boolean.TRUE.equals(enabled);
}

/**
* Returns whether {@code @ActivityImpl}-annotated beans should be automatically registered with
* matching workers. Defaults to {@code true} when {@link #enabled} is {@code true} or when the
* deprecated {@link #packages} property is set and non-empty; {@code false} otherwise.
*/
public boolean isRegisterActivityBeans() {
if (registerActivityBeans != null) return registerActivityBeans;
return Boolean.TRUE.equals(enabled) || (packages != null && !packages.isEmpty());
}

/**
* Returns whether {@code @NexusServiceImpl}-annotated beans should be automatically registered
* with matching workers. Defaults to {@code true} when {@link #enabled} is {@code true} or when
* the deprecated {@link #packages} property is set and non-empty; {@code false} otherwise.
*/
public boolean isRegisterNexusServiceBeans() {
if (registerNexusServiceBeans != null) return registerNexusServiceBeans;
return Boolean.TRUE.equals(enabled) || (packages != null && !packages.isEmpty());
}

/**
* Returns the list of packages to scan for {@code @WorkflowImpl} classes. When the deprecated
* {@link #packages} property is set, its entries are used; otherwise returns the entries of
* {@link #workflowPackages}. The two properties cannot be set simultaneously.
*/
public List<String> getEffectiveWorkflowPackages() {
List<String> result = new ArrayList<>();
if (packages != null) result.addAll(packages);
if (workflowPackages != null) result.addAll(workflowPackages);
return result;
}

@Nullable
public List<String> getWorkflowPackages() {
return workflowPackages;
}

/**
* @deprecated `packages` has unclear semantics with regard to registration of activity and nexus
* service beans; use {@link #getWorkflowPackages()} instead.
*/
@Deprecated
@DeprecatedConfigurationProperty(
replacement = "spring.temporal.workers-auto-discovery.workflow-packages",
reason =
"'packages' has unclear semantics with regard to registration of activity and nexus service beans; use 'workflow-packages' instead")
@Nullable
public List<String> getPackages() {
return packages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.temporal.spring.boot.WorkflowImpl;
import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties;
import io.temporal.spring.boot.autoconfigure.properties.WorkerProperties;
import io.temporal.spring.boot.autoconfigure.properties.WorkersAutoDiscoveryProperties;
import io.temporal.worker.*;
import io.temporal.workflow.DynamicWorkflow;
import java.lang.reflect.Constructor;
Expand Down Expand Up @@ -179,22 +180,40 @@ private Collection<Worker> createWorkers(WorkerFactory workerFactory) {
createWorkerFromAnExplicitConfig(workerFactory, workerProperties, workers));
}

if (namespaceProperties.getWorkersAutoDiscovery() != null
&& namespaceProperties.getWorkersAutoDiscovery().getPackages() != null) {
Collection<Class<?>> autoDiscoveredWorkflowImplementationClasses =
autoDiscoverWorkflowImplementations();
Map<String, Object> autoDiscoveredActivityBeans = autoDiscoverActivityBeans();
Map<String, Object> autoDiscoveredNexusServiceBeans = autoDiscoverNexusServiceBeans();
WorkersAutoDiscoveryProperties autoDiscovery = namespaceProperties.getWorkersAutoDiscovery();
if (autoDiscovery != null) {
// @ActivityImpl beans are Spring beans, discoverable without classpath scanning.
if (autoDiscovery.isRegisterActivityBeans()) {
Map<String, Object> autoDiscoveredActivityBeans = autoDiscoverActivityBeans();
configureActivityBeansByTaskQueue(workerFactory, workers, autoDiscoveredActivityBeans);
configureActivityBeansByWorkerName(workers, autoDiscoveredActivityBeans);
}

// @NexusServiceImpl beans are Spring beans, discoverable without classpath scanning.
if (autoDiscovery.isRegisterNexusServiceBeans()) {
Map<String, Object> autoDiscoveredNexusServiceBeans = autoDiscoverNexusServiceBeans();
configureNexusServiceBeansByTaskQueue(
workerFactory, workers, autoDiscoveredNexusServiceBeans);
configureNexusServiceBeansByWorkerName(workers, autoDiscoveredNexusServiceBeans);
}

configureWorkflowImplementationsByTaskQueue(
workerFactory, workers, autoDiscoveredWorkflowImplementationClasses);
configureActivityBeansByTaskQueue(workerFactory, workers, autoDiscoveredActivityBeans);
configureNexusServiceBeansByTaskQueue(
workerFactory, workers, autoDiscoveredNexusServiceBeans);
configureWorkflowImplementationsByWorkerName(
workers, autoDiscoveredWorkflowImplementationClasses);
configureActivityBeansByWorkerName(workers, autoDiscoveredActivityBeans);
configureNexusServiceBeansByWorkerName(workers, autoDiscoveredNexusServiceBeans);
// Workflow discovery: Spring-bean-based (enabled: true) and/or classpath-scanning (packages).
// These two sources are unioned; duplicates are harmless because Set is used internally.
Set<Class<?>> autoDiscoveredWorkflowImplementationClasses = new HashSet<>();
if (autoDiscovery.isRegisterWorkflowManagedBeans()) {
autoDiscoveredWorkflowImplementationClasses.addAll(autoDiscoverWorkflowBeans());
}
List<String> workflowPackages = autoDiscovery.getEffectiveWorkflowPackages();
if (!workflowPackages.isEmpty()) {
autoDiscoveredWorkflowImplementationClasses.addAll(
autoDiscoverWorkflowImplementations(workflowPackages));
}
if (!autoDiscoveredWorkflowImplementationClasses.isEmpty()) {
configureWorkflowImplementationsByTaskQueue(
workerFactory, workers, autoDiscoveredWorkflowImplementationClasses);
configureWorkflowImplementationsByWorkerName(
workers, autoDiscoveredWorkflowImplementationClasses);
}
}

return workers.getWorkers();
Expand Down Expand Up @@ -350,12 +369,12 @@ private void configureNexusServiceBeansByWorkerName(
});
}

private Collection<Class<?>> autoDiscoverWorkflowImplementations() {
private Collection<Class<?>> autoDiscoverWorkflowImplementations(List<String> packages) {
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false, environment);
scanner.addIncludeFilter(new AnnotationTypeFilter(WorkflowImpl.class));
Set<Class<?>> implementations = new HashSet<>();
for (String pckg : namespaceProperties.getWorkersAutoDiscovery().getPackages()) {
for (String pckg : packages) {
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(pckg);
for (BeanDefinition beanDefinition : candidateComponents) {
try {
Expand All @@ -369,6 +388,12 @@ private Collection<Class<?>> autoDiscoverWorkflowImplementations() {
return implementations;
}

private Collection<Class<?>> autoDiscoverWorkflowBeans() {
return beanFactory.getBeansWithAnnotation(WorkflowImpl.class).values().stream()
.map(AopUtils::getTargetClass)
.collect(java.util.stream.Collectors.toSet());
}

private Map<String, Object> autoDiscoverActivityBeans() {
return beanFactory.getBeansWithAnnotation(ActivityImpl.class);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.temporal.spring.boot.autoconfigure;

import static org.junit.jupiter.api.Assertions.*;

import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.test.context.ActiveProfiles;

/**
* Regression test for https://github.com/temporalio/sdk-java/issues/2780:
* {@code @ActivityImpl}-annotated beans should be auto-registered with workers even when no
* workflow packages are configured under {@code spring.temporal.workers-auto-discovery.packages}.
*/
@SpringBootTest(classes = AutoDiscoveryActivitiesOnlyTest.Configuration.class)
@ActiveProfiles(profiles = "auto-discovery-activities-only")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class AutoDiscoveryActivitiesOnlyTest {

@Autowired ConfigurableApplicationContext applicationContext;

@Autowired private WorkersTemplate workersTemplate;

@BeforeEach
void setUp() {
applicationContext.start();
}

@Test
@Timeout(value = 10)
public void testActivityBeansRegisteredWithoutWorkflowPackages() {
assertNotNull(workersTemplate);
Map<String, WorkersTemplate.RegisteredInfo> registeredInfoMap =
workersTemplate.getRegisteredInfo();

// One worker should have been created for the task queue specified in @ActivityImpl
assertEquals(1, registeredInfoMap.size());
registeredInfoMap.forEach(
(taskQueue, info) -> {
assertEquals("UnitTest", taskQueue);

// No workflow packages configured, so no workflows should be registered
assertTrue(
info.getRegisteredWorkflowInfo().isEmpty(),
"No workflows expected when packages: [] is configured");

// @ActivityImpl bean should be registered despite no packages being configured
assertFalse(
info.getRegisteredActivityInfo().isEmpty(),
"@ActivityImpl beans should be auto-registered without workflow packages");
assertEquals(1, info.getRegisteredActivityInfo().size());
assertEquals(
"io.temporal.spring.boot.autoconfigure.bytaskqueue.TestActivityImpl",
info.getRegisteredActivityInfo().get(0).getClassName());

// @NexusServiceImpl bean should also be registered
assertFalse(
info.getRegisteredNexusServiceInfos().isEmpty(),
"@NexusServiceImpl beans should be auto-registered without workflow packages");
assertEquals(1, info.getRegisteredNexusServiceInfos().size());
});
}

@ComponentScan(
excludeFilters =
@ComponentScan.Filter(
pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.byworkername\\..*",
type = FilterType.REGEX))
public static class Configuration {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.temporal.spring.boot.autoconfigure;

import io.temporal.api.nexus.v1.Endpoint;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestWorkflow;
import io.temporal.testing.TestWorkflowEnvironment;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.test.context.ActiveProfiles;

/**
* Verifies that the deprecated {@code workers-auto-discovery.packages} property still correctly
* registers workflows, activities, and nexus services (backward compatibility).
*/
@SpringBootTest(classes = AutoDiscoveryByTaskQueueLegacyTest.Configuration.class)
@ActiveProfiles(profiles = "auto-discovery-by-task-queue-legacy")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class AutoDiscoveryByTaskQueueLegacyTest {
@Autowired ConfigurableApplicationContext applicationContext;

@Autowired TestWorkflowEnvironment testWorkflowEnvironment;

@Autowired WorkflowClient workflowClient;
Endpoint endpoint;

@BeforeEach
void setUp() {
applicationContext.start();
endpoint =
testWorkflowEnvironment.createNexusEndpoint("AutoDiscoveryByTaskQueueEndpoint", "UnitTest");
}

@AfterEach
void tearDown() {
testWorkflowEnvironment.deleteNexusEndpoint(endpoint);
}

@Test
@Timeout(value = 10)
public void testAutoDiscovery() {
TestWorkflow testWorkflow =
workflowClient.newWorkflowStub(
TestWorkflow.class, WorkflowOptions.newBuilder().setTaskQueue("UnitTest").build());
testWorkflow.execute("nexus");
}

@ComponentScan(
excludeFilters =
@ComponentScan.Filter(
pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.byworkername\\..*",
type = FilterType.REGEX))
public static class Configuration {}
}
Loading
Loading