This is an automated email from the ASF dual-hosted git repository. chrisdutz pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit ab894d4b9ed2da7b2b14d41fa5ee56dadaa12fcf Author: Christofer Dutz <[email protected]> AuthorDate: Thu Aug 13 12:17:01 2026 +0200 feat: Added an intervalMillis to the event pump. --- .../tools/eventpump/config/EventPumpFactory.java | 32 ++++- .../eventpump/config/TriggerConfiguration.java | 55 +++++++- .../eventpump/config/EventPumpFactoryTest.java | 147 +++++++++++++++++++++ .../modules/users/pages/tools/event-pump.adoc | 15 +++ 4 files changed, 242 insertions(+), 7 deletions(-) diff --git a/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactory.java b/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactory.java index a3a121a9fb..73f58f7259 100644 --- a/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactory.java +++ b/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactory.java @@ -158,11 +158,35 @@ public class EventPumpFactory { String type = config.getType(); if ("timer".equalsIgnoreCase(type)) { - // Timer trigger - long interval = config.getIntervalSeconds(); - long initialDelay = config.getInitialDelaySeconds() != null ? config.getInitialDelaySeconds() : 0; + // Timer trigger - the interval may be given in seconds or, for sub-second + // polling, in milliseconds. Accepting both and silently picking one would leave + // the reader of the config guessing at the actual rate, so it's rejected. + if (config.getIntervalSeconds() != null && config.getIntervalMillis() != null) { + throw new IllegalArgumentException( + "Timer trigger has both intervalSeconds (" + config.getIntervalSeconds() + + ") and intervalMillis (" + config.getIntervalMillis() + ") set - use one of them"); + } + if (config.getInitialDelaySeconds() != null && config.getInitialDelayMillis() != null) { + throw new IllegalArgumentException( + "Timer trigger has both initialDelaySeconds (" + config.getInitialDelaySeconds() + + ") and initialDelayMillis (" + config.getInitialDelayMillis() + ") set - use one of them"); + } + if (config.getIntervalSeconds() == null && config.getIntervalMillis() == null) { + throw new IllegalArgumentException( + "Timer trigger requires either intervalSeconds or intervalMillis"); + } + + if (config.getIntervalMillis() != null) { + long initialDelay = config.getInitialDelayMillis() != null ? config.getInitialDelayMillis() + : TimeUnit.SECONDS.toMillis(config.getInitialDelaySeconds() != null ? config.getInitialDelaySeconds() : 0); + return new TimerTrigger(config.getIntervalMillis(), initialDelay, TimeUnit.MILLISECONDS); + } + + long initialDelay = config.getInitialDelayMillis() != null ? config.getInitialDelayMillis() + : TimeUnit.SECONDS.toMillis(config.getInitialDelaySeconds() != null ? config.getInitialDelaySeconds() : 0); - return new TimerTrigger(interval, initialDelay, TimeUnit.SECONDS); + return new TimerTrigger(TimeUnit.SECONDS.toMillis(config.getIntervalSeconds()), initialDelay, + TimeUnit.MILLISECONDS); } else if ("subscription".equalsIgnoreCase(type)) { // Subscription trigger (placeholder) diff --git a/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/TriggerConfiguration.java b/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/TriggerConfiguration.java index 096a125281..fc225e6735 100644 --- a/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/TriggerConfiguration.java +++ b/plc4j/tools/event-pump/src/main/java/org/apache/plc4x/java/tools/eventpump/config/TriggerConfiguration.java @@ -25,7 +25,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; * Configuration for a trigger. * <p> * The type field determines the trigger type: - * - "timer": Time-based trigger (requires intervalSeconds) + * - "timer": Time-based trigger (requires either intervalSeconds or intervalMillis, not both) * - "subscription": Subscription-based trigger (requires tagName and tagAddress) */ public class TriggerConfiguration { @@ -37,9 +37,18 @@ public class TriggerConfiguration { @JsonProperty("intervalSeconds") private Long intervalSeconds; + // Sub-second polling is common, so the interval can alternatively be given in + // milliseconds. Setting both units on the same trigger is rejected when the trigger is + // built, rather than silently picking one of them. + @JsonProperty("intervalMillis") + private Long intervalMillis; + @JsonProperty("initialDelaySeconds") private Long initialDelaySeconds; + @JsonProperty("initialDelayMillis") + private Long initialDelayMillis; + // Subscription trigger properties @JsonProperty("tagName") private String tagName; @@ -78,7 +87,8 @@ public class TriggerConfiguration { } /** - * Set the interval in seconds (for timer triggers). + * Set the interval in seconds (for timer triggers). Mutually exclusive with + * {@link #setIntervalMillis(Long)} - setting both is a configuration error. * * @param intervalSeconds The interval in seconds */ @@ -86,6 +96,25 @@ public class TriggerConfiguration { this.intervalSeconds = intervalSeconds; } + /** + * Get the interval in milliseconds (for timer triggers). + * + * @return The interval in milliseconds, or null if it is configured in seconds + */ + public Long getIntervalMillis() { + return intervalMillis; + } + + /** + * Set the interval in milliseconds (for timer triggers). Mutually exclusive with + * {@link #setIntervalSeconds(Long)} - setting both is a configuration error. + * + * @param intervalMillis The interval in milliseconds + */ + public void setIntervalMillis(Long intervalMillis) { + this.intervalMillis = intervalMillis; + } + /** * Get the initial delay in seconds (for timer triggers). * @@ -96,7 +125,8 @@ public class TriggerConfiguration { } /** - * Set the initial delay in seconds (for timer triggers). + * Set the initial delay in seconds (for timer triggers). Mutually exclusive with + * {@link #setInitialDelayMillis(Long)} - setting both is a configuration error. * * @param initialDelaySeconds The initial delay in seconds */ @@ -104,6 +134,25 @@ public class TriggerConfiguration { this.initialDelaySeconds = initialDelaySeconds; } + /** + * Get the initial delay in milliseconds (for timer triggers). + * + * @return The initial delay in milliseconds, or null if it is configured in seconds + */ + public Long getInitialDelayMillis() { + return initialDelayMillis; + } + + /** + * Set the initial delay in milliseconds (for timer triggers). Mutually exclusive with + * {@link #setInitialDelaySeconds(Long)} - setting both is a configuration error. + * + * @param initialDelayMillis The initial delay in milliseconds + */ + public void setInitialDelayMillis(Long initialDelayMillis) { + this.initialDelayMillis = initialDelayMillis; + } + /** * Get the tag name (for subscription triggers). * diff --git a/plc4j/tools/event-pump/src/test/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactoryTest.java b/plc4j/tools/event-pump/src/test/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactoryTest.java index d4d2f64d74..778d598a19 100644 --- a/plc4j/tools/event-pump/src/test/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactoryTest.java +++ b/plc4j/tools/event-pump/src/test/java/org/apache/plc4x/java/tools/eventpump/config/EventPumpFactoryTest.java @@ -33,11 +33,13 @@ import org.apache.plc4x.java.api.model.PlcTag; import org.apache.plc4x.java.api.value.PlcValue; import org.apache.plc4x.java.tools.eventpump.EventPump; import org.apache.plc4x.java.tools.eventpump.TagBatch; +import org.apache.plc4x.java.tools.eventpump.triggers.TimerTrigger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -198,6 +200,151 @@ class EventPumpFactoryTest { assertTrue(exception.getMessage().contains("Connection 'nonexistent' not found")); } + /** + * Polling faster than once per second has to be expressible - it is the common case for + * PLC data collection, and the scraper this replaces configured its rate in milliseconds. + */ + @Test + void testCreateWithMillisecondInterval() throws Exception { + EventPumpConfiguration config = new EventPumpConfiguration(); + + ConnectionConfiguration connection = new ConnectionConfiguration(); + connection.setId("conn1"); + connection.setUrl("mock:test"); + config.getConnections().add(connection); + + BatchConfiguration batch = new BatchConfiguration(); + batch.setId("batch1"); + batch.setConnectionId("conn1"); + batch.setSimpleTags(Collections.singletonMap("tag1", "address1")); + + TriggerConfiguration trigger = new TriggerConfiguration(); + trigger.setType("timer"); + trigger.setIntervalMillis(250L); + trigger.setInitialDelayMillis(50L); + batch.setTrigger(trigger); + config.getBatches().add(batch); + + EventPump pump = EventPumpFactory.create(config, connectionManager, null); + + TimerTrigger timerTrigger = (TimerTrigger) pump.getBatch("batch1").getTrigger(); + assertEquals(250, timerTrigger.getIntervalMs()); + assertEquals(50, timerTrigger.getInitialDelayMs()); + pump.close(); + } + + /** + * Setting the interval in both units is ambiguous to whoever reads the config later, so it + * has to fail loudly rather than silently picking one. + */ + @Test + void testRejectsIntervalInBothUnits() { + EventPumpConfiguration config = configWithTrigger(trigger -> { + trigger.setIntervalSeconds(10L); + trigger.setIntervalMillis(100L); + }); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> EventPumpFactory.create(config, connectionManager, null) + ); + assertTrue(exception.getMessage().contains("intervalSeconds")); + assertTrue(exception.getMessage().contains("intervalMillis")); + // The message has to name the offending values, otherwise finding them in a large + // configuration file is guesswork. + assertTrue(exception.getMessage().contains("10")); + assertTrue(exception.getMessage().contains("100")); + } + + @Test + void testRejectsInitialDelayInBothUnits() { + EventPumpConfiguration config = configWithTrigger(trigger -> { + trigger.setIntervalMillis(100L); + trigger.setInitialDelaySeconds(5L); + trigger.setInitialDelayMillis(50L); + }); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> EventPumpFactory.create(config, connectionManager, null) + ); + assertTrue(exception.getMessage().contains("initialDelaySeconds")); + assertTrue(exception.getMessage().contains("initialDelayMillis")); + } + + /** + * Mixing the units across the two settings stays legal - only the same setting given twice + * is ambiguous. + */ + @Test + void testAllowsIntervalAndInitialDelayInDifferentUnits() throws Exception { + EventPumpConfiguration config = configWithTrigger(trigger -> { + trigger.setIntervalMillis(250L); + trigger.setInitialDelaySeconds(2L); + }); + + EventPump pump = EventPumpFactory.create(config, connectionManager, null); + + TimerTrigger timerTrigger = (TimerTrigger) pump.getBatch("batch1").getTrigger(); + assertEquals(250, timerTrigger.getIntervalMs()); + assertEquals(2000, timerTrigger.getInitialDelayMs()); + pump.close(); + } + + /** + * Builds a single-batch configuration whose timer trigger is set up by the given callback. + */ + private static EventPumpConfiguration configWithTrigger(java.util.function.Consumer<TriggerConfiguration> customizer) { + EventPumpConfiguration config = new EventPumpConfiguration(); + + ConnectionConfiguration connection = new ConnectionConfiguration(); + connection.setId("conn1"); + connection.setUrl("mock:test"); + config.getConnections().add(connection); + + BatchConfiguration batch = new BatchConfiguration(); + batch.setId("batch1"); + batch.setConnectionId("conn1"); + batch.setSimpleTags(Collections.singletonMap("tag1", "address1")); + + TriggerConfiguration trigger = new TriggerConfiguration(); + trigger.setType("timer"); + customizer.accept(trigger); + batch.setTrigger(trigger); + config.getBatches().add(batch); + return config; + } + + /** + * A timer trigger without any interval used to fail with a NullPointerException while + * unboxing the interval. + */ + @Test + void testCreateWithTimerTriggerWithoutInterval() { + EventPumpConfiguration config = new EventPumpConfiguration(); + + ConnectionConfiguration connection = new ConnectionConfiguration(); + connection.setId("conn1"); + connection.setUrl("mock:test"); + config.getConnections().add(connection); + + BatchConfiguration batch = new BatchConfiguration(); + batch.setId("batch1"); + batch.setConnectionId("conn1"); + batch.setSimpleTags(Collections.singletonMap("tag1", "address1")); + + TriggerConfiguration trigger = new TriggerConfiguration(); + trigger.setType("timer"); + batch.setTrigger(trigger); + config.getBatches().add(batch); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> EventPumpFactory.create(config, connectionManager, null) + ); + assertTrue(exception.getMessage().contains("intervalSeconds or intervalMillis")); + } + @Test void testCreateWithUnknownTriggerType() { // Arrange diff --git a/website/asciidoc/modules/users/pages/tools/event-pump.adoc b/website/asciidoc/modules/users/pages/tools/event-pump.adoc index 27dceec1a5..c4314ae2aa 100644 --- a/website/asciidoc/modules/users/pages/tools/event-pump.adoc +++ b/website/asciidoc/modules/users/pages/tools/event-pump.adoc @@ -152,6 +152,21 @@ pump.startAll(); The listener passed to the factory becomes the default listener for all batches in the file. +=== Trigger intervals + +A timer trigger takes its interval either in seconds or - for the sub-second rates that are common in PLC data collection - in milliseconds: + +[source,yaml] +---- + trigger: + type: timer + intervalMillis: 250 + initialDelayMillis: 100 +---- + +`intervalSeconds`/`initialDelaySeconds` and `intervalMillis`/`initialDelayMillis` are mutually exclusive per setting: giving the same setting in both units fails at startup rather than silently picking one, since a reader of the file would otherwise have to guess the actual rate. +Using seconds for one setting and milliseconds for the other is fine. + === Transformations A tag may carry an expression that is applied to its value before the listener sees it.
