Skip to content

Commit

Permalink
GH-8625: Add Duration support for <poller> (#8627)
Browse files Browse the repository at this point in the history
* GH-8625: Add Duration support for `<poller>`

Fixes #8625

The duration can be represented in a ISO 8601 format, e.g. `PT10S`, `P1D` etc.
The `<poller>` and `@Poller` don't support such a format.

* Introduce a `PeriodicTriggerFactoryBean` to accept string values for
trigger options and parse them manually before creating the target `PeriodicTrigger`
* Use this `PeriodicTriggerFactoryBean` in the `PollerParser` and `AbstractMethodAnnotationPostProcessor`
where we parse options for the `PeriodicTrigger`
* Modify tests to ensure that feature works
* Document the duration option
* Add more cross-links into polling docs
* Fix typos in the affected doc files
* Add `-parameters` for compiler options since SF 6.1 does not support `-debug` anymore
for method parameter names discovery

* Fix typos

Co-authored-by: Gary Russell <grussell@vmware.com>

---------

Co-authored-by: Gary Russell <grussell@vmware.com>
  • Loading branch information
artembilan and garyrussell committed May 22, 2023
1 parent ba417de commit c70d7a6
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 84 deletions.
1 change: 1 addition & 0 deletions build.gradle
Expand Up @@ -212,6 +212,7 @@ configure(javaProjects) { subproject ->

compileJava {
options.release = 17
options.compilerArgs << '-parameters'
}

compileTestJava {
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -65,16 +65,16 @@
String maxMessagesPerPoll() default "";

/**
* @return The fixed delay in milliseconds to create the
* {@link org.springframework.scheduling.support.PeriodicTrigger}. Can be specified as
* 'property placeholder', e.g. {@code ${poller.fixedDelay}}.
* @return The fixed delay in milliseconds or a {@link java.time.Duration} compliant string
* to create the {@link org.springframework.scheduling.support.PeriodicTrigger}.
* Can be specified as 'property placeholder', e.g. {@code ${poller.fixedDelay}}.
*/
String fixedDelay() default "";

/**
* @return The fixed rate in milliseconds to create the
* {@link org.springframework.scheduling.support.PeriodicTrigger} with
* {@code fixedRate}. Can be specified as 'property placeholder', e.g.
* @return The fixed rate in milliseconds or a {@link java.time.Duration} compliant string
* to create the {@link org.springframework.scheduling.support.PeriodicTrigger} with
* the {@code fixedRate} option. Can be specified as 'property placeholder', e.g.
* {@code ${poller.fixedRate}}.
*/
String fixedRate() default "";
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,7 +18,6 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -744,14 +743,11 @@ else if (StringUtils.hasText(cron)) {
"The '@Poller' 'cron' attribute is mutually exclusive with other attributes.");
trigger = new CronTrigger(cron);
}
else if (StringUtils.hasText(fixedDelayValue)) {
Assert.state(!StringUtils.hasText(fixedRateValue),
"The '@Poller' 'fixedDelay' attribute is mutually exclusive with other attributes.");
trigger = new PeriodicTrigger(Duration.ofMillis(Long.parseLong(fixedDelayValue)));
}
else if (StringUtils.hasText(fixedRateValue)) {
trigger = new PeriodicTrigger(Duration.ofMillis(Long.parseLong(fixedRateValue)));
((PeriodicTrigger) trigger).setFixedRate(true);
else if (StringUtils.hasText(fixedDelayValue) || StringUtils.hasText(fixedRateValue)) {
PeriodicTriggerFactoryBean periodicTriggerFactoryBean = new PeriodicTriggerFactoryBean();
periodicTriggerFactoryBean.setFixedDelayValue(fixedDelayValue);
periodicTriggerFactoryBean.setFixedRateValue(fixedRateValue);
trigger = periodicTriggerFactoryBean.getObject();
}
//'Trigger' can be null. 'PollingConsumer' does fallback to the 'new PeriodicTrigger(10)'.
pollerMetadata.setTrigger(trigger);
Expand Down
@@ -0,0 +1,115 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.integration.config;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* The {@link FactoryBean} to produce a {@link PeriodicTrigger}
* based on parsing string values for its options.
* This class is mostly driven by the XML configuration requirements for
* {@link Duration} value representations for the respective attributes.
*
* @author Artem Bilan
*
* @since 6.2
*/
public class PeriodicTriggerFactoryBean implements FactoryBean<PeriodicTrigger> {

@Nullable
private String fixedDelayValue;

@Nullable
private String fixedRateValue;

@Nullable
private String initialDelayValue;

@Nullable
private TimeUnit timeUnit;

public void setFixedDelayValue(String fixedDelayValue) {
this.fixedDelayValue = fixedDelayValue;
}

public void setFixedRateValue(String fixedRateValue) {
this.fixedRateValue = fixedRateValue;
}

public void setInitialDelayValue(String initialDelayValue) {
this.initialDelayValue = initialDelayValue;
}

public void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}

@Override
public PeriodicTrigger getObject() {
boolean hasFixedDelay = StringUtils.hasText(this.fixedDelayValue);
boolean hasFixedRate = StringUtils.hasText(this.fixedRateValue);

Assert.isTrue(hasFixedDelay ^ hasFixedRate,
"One of the 'fixedDelayValue' or 'fixedRateValue' property must be provided but not both.");

TimeUnit timeUnitToUse = this.timeUnit;
if (timeUnitToUse == null) {
timeUnitToUse = TimeUnit.MILLISECONDS;
}

Duration duration = toDuration(hasFixedDelay ? this.fixedDelayValue : this.fixedRateValue, timeUnitToUse);

PeriodicTrigger periodicTrigger = new PeriodicTrigger(duration);
periodicTrigger.setFixedRate(hasFixedRate);
if (StringUtils.hasText(this.initialDelayValue)) {
periodicTrigger.setInitialDelay(toDuration(this.initialDelayValue, timeUnitToUse));
}
return periodicTrigger;
}

@Override
public Class<?> getObjectType() {
return PeriodicTrigger.class;
}

private static Duration toDuration(String value, TimeUnit timeUnit) {
if (isDurationString(value)) {
return Duration.parse(value);
}
return toDuration(Long.parseLong(value), timeUnit);
}

private static boolean isDurationString(String value) {
return (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1))));
}

private static boolean isP(char ch) {
return (ch == 'P' || ch == 'p');
}

private static Duration toDuration(long value, TimeUnit timeUnit) {
return Duration.of(value, timeUnit.toChronoUnit());
}

}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,9 +28,9 @@
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.config.PeriodicTriggerFactoryBean;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;

Expand All @@ -45,12 +45,15 @@
*/
public class PollerParser extends AbstractBeanDefinitionParser {

private static final String MULTIPLE_TRIGGER_DEFINITIONS = "A <poller> cannot specify more than one trigger configuration.";
private static final String MULTIPLE_TRIGGER_DEFINITIONS =
"A <poller> cannot specify more than one trigger configuration.";

private static final String NO_TRIGGER_DEFINITIONS = "A <poller> must have one and only one trigger configuration.";

@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) throws BeanDefinitionStoreException {
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {

String id = super.resolveId(element, definition, parserContext);
if (element.getAttribute("default").equals("true")) {
if (parserContext.getRegistry().isBeanNameInUse(PollerMetadata.DEFAULT_POLLER_METADATA_BEAN_NAME)) {
Expand Down Expand Up @@ -102,29 +105,30 @@ else if (adviceChainElement != null) {

String errorChannel = element.getAttribute("error-channel");
if (StringUtils.hasText(errorChannel)) {
BeanDefinitionBuilder errorHandler = BeanDefinitionBuilder.genericBeanDefinition(MessagePublishingErrorHandler.class);
BeanDefinitionBuilder errorHandler =
BeanDefinitionBuilder.genericBeanDefinition(MessagePublishingErrorHandler.class);
errorHandler.addPropertyReference("defaultErrorChannel", errorChannel);
metadataBuilder.addPropertyValue("errorHandler", errorHandler.getBeanDefinition());
}
return metadataBuilder.getBeanDefinition();
}

private void configureTrigger(Element pollerElement, BeanDefinitionBuilder targetBuilder, ParserContext parserContext) {
private void configureTrigger(Element pollerElement, BeanDefinitionBuilder targetBuilder,
ParserContext parserContext) {

String triggerAttribute = pollerElement.getAttribute("trigger");
String fixedRateAttribute = pollerElement.getAttribute("fixed-rate");
String fixedDelayAttribute = pollerElement.getAttribute("fixed-delay");
String cronAttribute = pollerElement.getAttribute("cron");
String timeUnit = pollerElement.getAttribute("time-unit");

List<String> triggerBeanNames = new ArrayList<String>();
List<String> triggerBeanNames = new ArrayList<>();
if (StringUtils.hasText(triggerAttribute)) {
trigger(pollerElement, parserContext, triggerAttribute, timeUnit, triggerBeanNames);
}
if (StringUtils.hasText(fixedRateAttribute)) {
fixedRate(parserContext, fixedRateAttribute, timeUnit, triggerBeanNames);
}
if (StringUtils.hasText(fixedDelayAttribute)) {
fixedDelay(parserContext, fixedDelayAttribute, timeUnit, triggerBeanNames);
if (StringUtils.hasText(fixedRateAttribute) || StringUtils.hasText(fixedDelayAttribute)) {
period(parserContext, fixedDelayAttribute, fixedRateAttribute, pollerElement.getAttribute("initial-delay"),
timeUnit, triggerBeanNames);
}
if (StringUtils.hasText(cronAttribute)) {
cron(pollerElement, parserContext, cronAttribute, timeUnit, triggerBeanNames);
Expand All @@ -142,33 +146,20 @@ private void trigger(Element pollerElement, ParserContext parserContext, String
List<String> triggerBeanNames) {

if (StringUtils.hasText(timeUnit)) {
parserContext.getReaderContext().error("The 'time-unit' attribute cannot be used with a 'trigger' reference.", pollerElement);
parserContext.getReaderContext()
.error("The 'time-unit' attribute cannot be used with a 'trigger' reference.", pollerElement);
}
triggerBeanNames.add(triggerAttribute);
}

private void fixedRate(ParserContext parserContext, String fixedRateAttribute, String timeUnit,
List<String> triggerBeanNames) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTrigger.class);
builder.addConstructorArgValue(fixedRateAttribute);
if (StringUtils.hasText(timeUnit)) {
builder.addConstructorArgValue(timeUnit);
}
builder.addPropertyValue("fixedRate", Boolean.TRUE);
String triggerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
triggerBeanNames.add(triggerBeanName);
}

private void fixedDelay(ParserContext parserContext, String fixedDelayAttribute, String timeUnit,
List<String> triggerBeanNames) {
private void period(ParserContext parserContext, String fixedDelayAttribute, String fixedRateAttribute,
String initialDelayAttribute, String timeUnit, List<String> triggerBeanNames) {

BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTrigger.class);
builder.addConstructorArgValue(fixedDelayAttribute);
if (StringUtils.hasText(timeUnit)) {
builder.addConstructorArgValue(timeUnit);
}
builder.addPropertyValue("fixedRate", Boolean.FALSE);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(PeriodicTriggerFactoryBean.class);
builder.addPropertyValue("fixedDelayValue", fixedDelayAttribute);
builder.addPropertyValue("fixedRateValue", fixedRateAttribute);
builder.addPropertyValue("timeUnit", timeUnit);
builder.addPropertyValue("initialDelayValue", initialDelayAttribute);
String triggerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
triggerBeanNames.add(triggerBeanName);
Expand All @@ -187,4 +178,5 @@ private void cron(Element pollerElement, ParserContext parserContext, String cro
builder.getBeanDefinition(), parserContext.getRegistry());
triggerBeanNames.add(triggerBeanName);
}

}
Expand Up @@ -1983,7 +1983,7 @@
</xsd:sequence>
<xsd:attribute name="fixed-delay" type="xsd:string">
<xsd:annotation>
<xsd:documentation>Fixed delay trigger (in milliseconds).</xsd:documentation>
<xsd:documentation>Fixed delay trigger (decimal for time unit or Duration string).</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="ref" type="xsd:string" use="optional">
Expand All @@ -1997,7 +1997,14 @@
</xsd:attribute>
<xsd:attribute name="fixed-rate" type="xsd:string">
<xsd:annotation>
<xsd:documentation>Fixed rate trigger (in milliseconds).</xsd:documentation>
<xsd:documentation>Fixed rate trigger (decimal for time unit or Duration string).</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="initial-delay" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Periodic trigger initial delay (decimal for time unit or Duration string).
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="time-unit">
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,7 +16,7 @@

package org.springframework.integration.config.xml;

import java.time.temporal.ChronoUnit;
import java.time.Duration;
import java.util.HashMap;

import org.aopalliance.aop.Advice;
Expand Down Expand Up @@ -112,7 +112,9 @@ public void pollerWithReceiveTimeoutAndTimeunit() {
PollerMetadata metadata = (PollerMetadata) poller;
assertThat(metadata.getReceiveTimeout()).isEqualTo(1234);
PeriodicTrigger trigger = (PeriodicTrigger) metadata.getTrigger();
assertThat(TestUtils.getPropertyValue(trigger, "chronoUnit")).isEqualTo(ChronoUnit.SECONDS);
assertThat(trigger.getPeriodDuration()).isEqualTo(Duration.ofSeconds(5));
assertThat(trigger.isFixedRate()).isTrue();
assertThat(trigger.getInitialDelayDuration()).isEqualTo(Duration.ofSeconds(45));
context.close();
}

Expand All @@ -123,7 +125,7 @@ public void pollerWithTriggerReference() {
Object poller = context.getBean("poller");
assertThat(poller).isNotNull();
PollerMetadata metadata = (PollerMetadata) poller;
assertThat(metadata.getTrigger() instanceof TestTrigger).isTrue();
assertThat(metadata.getTrigger()).isInstanceOf(TestTrigger.class);
context.close();
}

Expand Down
Expand Up @@ -15,6 +15,6 @@
<beans:prop key="seconds">SECONDS</beans:prop>
</util:properties>

<poller id="poller" receive-timeout="1234" fixed-rate="5" time-unit="${seconds}"/>
<poller id="poller" receive-timeout="1234" fixed-rate="5" time-unit="${seconds}" initial-delay="PT45S"/>

</beans:beans>
@@ -1,5 +1,5 @@
message.history.tracked.components=input, publishedChannel, annotationTestService*
poller.maxMessagesPerPoll=10
poller.interval=100
poller.interval=PT0.1S
poller.receiveTimeout=10000
global.wireTap.pattern=input

0 comments on commit c70d7a6

Please sign in to comment.