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.

Reply via email to