Skip to content

Commit c6375d6

Browse files
authored
feat: multi namespace support (#2378)
feat: multi namespace support
1 parent c6e0306 commit c6375d6

15 files changed

+884
-45
lines changed

temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,22 @@
2020

2121
package io.temporal.spring.boot.autoconfigure;
2222

23+
import com.google.common.base.MoreObjects;
2324
import io.temporal.common.converter.DataConverter;
25+
import io.temporal.spring.boot.TemporalOptionsCustomizer;
26+
import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties;
27+
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
28+
import java.util.ArrayList;
2429
import java.util.List;
30+
import java.util.Map;
31+
import java.util.Map.Entry;
32+
import java.util.Objects;
33+
import java.util.stream.Collectors;
2534
import javax.annotation.Nullable;
2635
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
2736

2837
class AutoConfigurationUtils {
38+
2939
@Nullable
3040
static DataConverter choseDataConverter(
3141
List<DataConverter> dataConverters, DataConverter mainDataConverter) {
@@ -46,4 +56,76 @@ static DataConverter choseDataConverter(
4656
}
4757
return chosenDataConverter;
4858
}
59+
60+
@Nullable
61+
static DataConverter choseDataConverter(
62+
Map<String, DataConverter> dataConverters,
63+
DataConverter mainDataConverter,
64+
TemporalProperties properties) {
65+
if (Objects.isNull(dataConverters) || dataConverters.isEmpty()) {
66+
return null;
67+
}
68+
List<NonRootNamespaceProperties> nonRootNamespaceProperties = properties.getNamespaces();
69+
if (Objects.isNull(nonRootNamespaceProperties) || nonRootNamespaceProperties.isEmpty()) {
70+
return choseDataConverter(new ArrayList<>(dataConverters.values()), mainDataConverter);
71+
} else {
72+
List<DataConverter> dataConverterList = new ArrayList<>();
73+
List<String> nonRootBeanNames =
74+
nonRootNamespaceProperties.stream()
75+
.map(
76+
ns ->
77+
MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace())
78+
+ DataConverter.class.getSimpleName())
79+
.collect(Collectors.toList());
80+
for (Entry<String, DataConverter> dataConverterEntry : dataConverters.entrySet()) {
81+
String beanName = dataConverterEntry.getKey();
82+
DataConverter dataConverter = dataConverterEntry.getValue();
83+
if (beanName.equals("mainDataConverter")) {
84+
continue;
85+
}
86+
// Indicate its non-root namespace data converter, skip it
87+
if (nonRootBeanNames.contains(beanName)) {
88+
continue;
89+
}
90+
dataConverterList.add(dataConverter);
91+
}
92+
return choseDataConverter(dataConverterList, mainDataConverter);
93+
}
94+
}
95+
96+
static <T> TemporalOptionsCustomizer<T> chooseTemporalCustomizerBean(
97+
Map<String, TemporalOptionsCustomizer<T>> customizerMap,
98+
Class<T> genericOptionsBuilderClass,
99+
TemporalProperties properties) {
100+
if (Objects.isNull(customizerMap) || customizerMap.isEmpty()) {
101+
return null;
102+
}
103+
List<NonRootNamespaceProperties> nonRootNamespaceProperties = properties.getNamespaces();
104+
if (Objects.isNull(nonRootNamespaceProperties) || nonRootNamespaceProperties.isEmpty()) {
105+
return customizerMap.values().stream().findFirst().orElse(null);
106+
}
107+
// Non-root namespace bean names, such as "nsWorkerFactoryCustomizer", "nsWorkerCustomizer"
108+
List<String> nonRootBeanNames =
109+
nonRootNamespaceProperties.stream()
110+
.map(
111+
ns ->
112+
temporalCustomizerBeanName(
113+
MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace()),
114+
genericOptionsBuilderClass))
115+
.collect(Collectors.toList());
116+
117+
return customizerMap.entrySet().stream()
118+
.filter(entry -> !nonRootBeanNames.contains(entry.getKey()))
119+
.findFirst()
120+
.map(Entry::getValue)
121+
.orElse(null);
122+
}
123+
124+
static String temporalCustomizerBeanName(String beanPrefix, Class<?> optionsBuilderClass) {
125+
String builderCanonicalName = optionsBuilderClass.getCanonicalName();
126+
String bindingCustomizerName = builderCanonicalName.replace("Options.Builder", "Customizer");
127+
bindingCustomizerName =
128+
bindingCustomizerName.substring(bindingCustomizerName.lastIndexOf(".") + 1);
129+
return beanPrefix + bindingCustomizerName;
130+
}
49131
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
3+
*
4+
* Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this material except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package io.temporal.spring.boot.autoconfigure;
22+
23+
import com.google.common.base.MoreObjects;
24+
import com.uber.m3.tally.Scope;
25+
import io.opentracing.Tracer;
26+
import io.temporal.client.WorkflowClient;
27+
import io.temporal.client.WorkflowClientOptions;
28+
import io.temporal.client.schedules.ScheduleClient;
29+
import io.temporal.client.schedules.ScheduleClientOptions;
30+
import io.temporal.common.converter.DataConverter;
31+
import io.temporal.serviceclient.WorkflowServiceStubs;
32+
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
33+
import io.temporal.spring.boot.TemporalOptionsCustomizer;
34+
import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties;
35+
import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties;
36+
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
37+
import io.temporal.spring.boot.autoconfigure.template.ClientTemplate;
38+
import io.temporal.spring.boot.autoconfigure.template.NamespaceTemplate;
39+
import io.temporal.spring.boot.autoconfigure.template.NonRootNamespaceTemplate;
40+
import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate;
41+
import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter;
42+
import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate;
43+
import io.temporal.worker.WorkerFactory;
44+
import io.temporal.worker.WorkerFactoryOptions.Builder;
45+
import io.temporal.worker.WorkerOptions;
46+
import io.temporal.worker.WorkflowImplementationOptions;
47+
import java.util.List;
48+
import java.util.Optional;
49+
import javax.annotation.Nonnull;
50+
import javax.annotation.Nullable;
51+
import org.slf4j.Logger;
52+
import org.slf4j.LoggerFactory;
53+
import org.springframework.beans.BeansException;
54+
import org.springframework.beans.factory.BeanFactory;
55+
import org.springframework.beans.factory.BeanFactoryAware;
56+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
57+
import org.springframework.beans.factory.config.BeanPostProcessor;
58+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
59+
60+
public class NonRootBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
61+
62+
private static final Logger log = LoggerFactory.getLogger(NonRootBeanPostProcessor.class);
63+
64+
private ConfigurableListableBeanFactory beanFactory;
65+
66+
private final @Nonnull TemporalProperties temporalProperties;
67+
private final @Nullable List<NonRootNamespaceProperties> namespaceProperties;
68+
private final @Nullable Tracer tracer;
69+
private final @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment;
70+
private final @Nullable Scope metricsScope;
71+
72+
public NonRootBeanPostProcessor(
73+
@Nonnull TemporalProperties temporalProperties,
74+
@Nullable Tracer tracer,
75+
@Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment,
76+
@Nullable Scope metricsScope) {
77+
this.temporalProperties = temporalProperties;
78+
this.namespaceProperties = temporalProperties.getNamespaces();
79+
this.tracer = tracer;
80+
this.testWorkflowEnvironment = testWorkflowEnvironment;
81+
this.metricsScope = metricsScope;
82+
}
83+
84+
@Override
85+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
86+
if (bean instanceof NamespaceTemplate && beanName.equals("temporalRootNamespaceTemplate")) {
87+
if (namespaceProperties != null) {
88+
namespaceProperties.forEach(this::injectBeanByNonRootNamespace);
89+
}
90+
}
91+
return bean;
92+
}
93+
94+
private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) {
95+
String beanPrefix = MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace());
96+
DataConverter dataConverterByNamespace = findBeanByNamespace(beanPrefix, DataConverter.class);
97+
98+
// found regarding namespace customizer bean, it can be optional
99+
TemporalOptionsCustomizer<Builder> workFactoryCustomizer =
100+
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, Builder.class);
101+
TemporalOptionsCustomizer<WorkflowServiceStubsOptions.Builder> workflowServiceStubsCustomizer =
102+
findBeanByNameSpaceForTemporalCustomizer(
103+
beanPrefix, WorkflowServiceStubsOptions.Builder.class);
104+
TemporalOptionsCustomizer<WorkerOptions.Builder> WorkerCustomizer =
105+
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkerOptions.Builder.class);
106+
TemporalOptionsCustomizer<WorkflowClientOptions.Builder> workflowClientCustomizer =
107+
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkflowClientOptions.Builder.class);
108+
TemporalOptionsCustomizer<ScheduleClientOptions.Builder> scheduleClientCustomizer =
109+
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, ScheduleClientOptions.Builder.class);
110+
TemporalOptionsCustomizer<WorkflowImplementationOptions.Builder>
111+
workflowImplementationCustomizer =
112+
findBeanByNameSpaceForTemporalCustomizer(
113+
beanPrefix, WorkflowImplementationOptions.Builder.class);
114+
115+
// it not set namespace connection properties, use root connection properties
116+
ConnectionProperties connectionProperties =
117+
MoreObjects.firstNonNull(ns.getConnection(), temporalProperties.getConnection());
118+
ServiceStubsTemplate serviceStubsTemplate =
119+
new ServiceStubsTemplate(
120+
connectionProperties,
121+
metricsScope,
122+
testWorkflowEnvironment,
123+
workflowServiceStubsCustomizer);
124+
WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs();
125+
126+
NonRootNamespaceTemplate namespaceTemplate =
127+
new NonRootNamespaceTemplate(
128+
beanFactory,
129+
ns,
130+
workflowServiceStubs,
131+
dataConverterByNamespace,
132+
tracer,
133+
testWorkflowEnvironment,
134+
workFactoryCustomizer,
135+
WorkerCustomizer,
136+
builder ->
137+
// Must make sure the namespace is set at the end of the builder chain
138+
Optional.ofNullable(workflowClientCustomizer)
139+
.map(c -> c.customize(builder))
140+
.orElse(builder)
141+
.setNamespace(ns.getNamespace()),
142+
scheduleClientCustomizer,
143+
workflowImplementationCustomizer);
144+
145+
ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate();
146+
WorkflowClient workflowClient = clientTemplate.getWorkflowClient();
147+
ScheduleClient scheduleClient = clientTemplate.getScheduleClient();
148+
WorkersTemplate workersTemplate = namespaceTemplate.getWorkersTemplate();
149+
WorkerFactory workerFactory = workersTemplate.getWorkerFactory();
150+
151+
// register beans by namespace
152+
beanFactory.registerSingleton(
153+
beanPrefix + ServiceStubsTemplate.class.getSimpleName(), serviceStubsTemplate);
154+
beanFactory.registerSingleton(
155+
beanPrefix + WorkflowServiceStubs.class.getSimpleName(), workflowServiceStubs);
156+
beanFactory.registerSingleton(
157+
beanPrefix + NamespaceTemplate.class.getSimpleName(), namespaceTemplate);
158+
beanFactory.registerSingleton(
159+
beanPrefix + ClientTemplate.class.getSimpleName(), namespaceTemplate.getClientTemplate());
160+
beanFactory.registerSingleton(
161+
beanPrefix + WorkersTemplate.class.getSimpleName(), workersTemplate);
162+
beanFactory.registerSingleton(
163+
beanPrefix + WorkflowClient.class.getSimpleName(), workflowClient);
164+
beanFactory.registerSingleton(
165+
beanPrefix + ScheduleClient.class.getSimpleName(), scheduleClient);
166+
beanFactory.registerSingleton(beanPrefix + WorkerFactory.class.getSimpleName(), workerFactory);
167+
}
168+
169+
@Override
170+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
171+
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
172+
}
173+
174+
private <T> T findBeanByNamespace(String beanPrefix, Class<T> clazz) {
175+
try {
176+
return beanFactory.getBean(beanPrefix + clazz.getSimpleName(), clazz);
177+
} catch (NoSuchBeanDefinitionException ignore) {
178+
// Made non-namespace bean optional
179+
}
180+
return null;
181+
}
182+
183+
private <T> TemporalOptionsCustomizer<T> findBeanByNameSpaceForTemporalCustomizer(
184+
String beanPrefix, Class<T> genericOptionsBuilderClass) {
185+
String beanName =
186+
AutoConfigurationUtils.temporalCustomizerBeanName(beanPrefix, genericOptionsBuilderClass);
187+
try {
188+
TemporalOptionsCustomizer genericOptionsCustomizer =
189+
beanFactory.getBean(beanName, TemporalOptionsCustomizer.class);
190+
return (TemporalOptionsCustomizer<T>) genericOptionsCustomizer;
191+
} catch (BeansException e) {
192+
log.warn("No TemporalOptionsCustomizer found for {}. ", beanName);
193+
if (genericOptionsBuilderClass.isAssignableFrom(Builder.class)) {
194+
// print tips once
195+
log.debug(
196+
"No TemporalOptionsCustomizer found for {}. \n You can add Customizer bean to do by namespace customization. \n "
197+
+ "Note: bean name should start with namespace name and end with Customizer, and the middle part should be the customizer "
198+
+ "target class name. \n "
199+
+ "Example: @Bean(\"nsWorkerFactoryCustomizer\") is a customizer bean for WorkerFactory via "
200+
+ "TemporalOptionsCustomizer<WorkerFactoryOptions.Builder>",
201+
genericOptionsBuilderClass.getSimpleName());
202+
}
203+
return null;
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)