diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3f7f76f..7fa6631 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,10 +25,10 @@ jobs:
os: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14, windows-2022, windows-2025]
steps:
- uses: actions/checkout@v4
- - name: Set up JDK 21
+ - name: Set up JDK 25
uses: actions/setup-java@v4
with:
- java-version: "21"
+ java-version: "25"
distribution: "temurin"
cache: maven
- name: Build with Maven
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 06634f7..9b31c45 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,10 +29,10 @@ jobs:
os: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14, windows-2022, windows-2025]
steps:
- uses: actions/checkout@v4
- - name: Set up JDK 21
+ - name: Set up JDK 25
uses: actions/setup-java@v4
with:
- java-version: '21'
+ java-version: '25'
distribution: 'temurin'
cache: maven
- name: Test with Maven
diff --git a/Dockerfile b/Dockerfile
index 986066e..678c901 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-FROM maven:3.9.9-eclipse-temurin-21 AS build
+FROM maven:3.9.11-eclipse-temurin-25 AS build
WORKDIR /workspace
COPY pom.xml ./
@@ -9,7 +9,7 @@ COPY src ./src
# Build a Spring Boot executable jar.
RUN mvn --batch-mode -DskipTests package
-FROM eclipse-temurin:21-jre
+FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=build /workspace/target/recceiver-*.jar /app/recceiver.jar
diff --git a/pom.xml b/pom.xml
index f185492..d23d6e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,11 +9,10 @@
4.7.2-SNAPSHOT
- 21
- 21
+ 25
+ 25
UTF-8
- 2.7.12
- 2.1.10
+ 4.1.0
@@ -41,11 +40,9 @@
com.fasterxml.jackson.core
jackson-databind
-
- javax.xml.bind
- jaxb-api
- 2.3.1
+ jakarta.annotation
+ jakarta.annotation-api
@@ -56,13 +53,6 @@
-
- org.springframework.shell
- spring-shell-dependencies
- ${spring-shell.version}
- pom
- import
-
org.springframework.boot
@@ -93,9 +83,10 @@
org.apache.maven.plugins
maven-compiler-plugin
+ 3.14.0
- 21
- 21
+ ${maven.compiler.source}
+ ${maven.compiler.target}
diff --git a/src/main/java/org/phoebus/recceiver/MessageProcessor.java b/src/main/java/org/phoebus/recceiver/MessageProcessor.java
index 223f655..534640d 100644
--- a/src/main/java/org/phoebus/recceiver/MessageProcessor.java
+++ b/src/main/java/org/phoebus/recceiver/MessageProcessor.java
@@ -16,12 +16,14 @@
import org.phoebus.recceiver.spi.RecSyncProcessorDispatcher;
import org.phoebus.recceiver.spi.RecSyncTransaction;
import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class MessageProcessor {
private static final Logger log = Logger.getLogger(MessageProcessor.class.getName());
+ private static final ThreadFactory COMMIT_THREAD_FACTORY = Thread.ofVirtual().name("recceiver-commit-", 0).factory();
private final SocketChannel channel;
private final String connectionSourceAddress;
@@ -355,8 +357,8 @@ private void dispatchTransaction() {
// pong replies during CF uploads. For large initial batches (100k+ channels) the upload
// can take minutes; blocking this thread would prevent any pings from being sent and the
// IOC would close the connection with an EOF before the upload finishes.
- Thread.ofVirtual().name("recceiver-commit-", 0).start(() ->
- processorDispatcher.dispatchCommit(transaction));
+ COMMIT_THREAD_FACTORY.newThread(() ->
+ processorDispatcher.dispatchCommit(transaction)).start();
}
private void notifyConnectionClosed(String reason) {
diff --git a/src/main/java/org/phoebus/recceiver/RecceiverApplication.java b/src/main/java/org/phoebus/recceiver/RecceiverApplication.java
index eed70b2..c0dd4c9 100644
--- a/src/main/java/org/phoebus/recceiver/RecceiverApplication.java
+++ b/src/main/java/org/phoebus/recceiver/RecceiverApplication.java
@@ -9,7 +9,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
-import javax.annotation.PreDestroy;
+import jakarta.annotation.PreDestroy;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
@@ -25,6 +25,7 @@
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -87,6 +88,9 @@ public class RecceiverApplication implements CommandLineRunner {
@Value("${recceiver.cf.password:}")
private String cfPassword;
+ @Value("${recceiver.cf.http-timeout:30}")
+ private String cfHttpTimeout;
+
@Value("${recceiver.cf.delete-on-clean:false}")
private String cfDeleteOnClean;
@@ -107,6 +111,7 @@ public class RecceiverApplication implements CommandLineRunner {
private final ScheduledExecutorService udpAnnouncerService = Executors.newScheduledThreadPool(1);
+ private static final ThreadFactory CLIENT_THREAD_FACTORY = Thread.ofVirtual().name("recceiver-client-", 0).factory();
private static int serverKey;
private RecSyncProcessorDispatcher processorDispatcher;
private volatile boolean running = true;
@@ -180,7 +185,7 @@ private void register(ServerSocketChannel serverSocket)
client.configureBlocking(true);
// Run each client handler on its own virtual thread.
- Thread.ofVirtual().name("recceiver-client-", 0).start(() -> {
+ CLIENT_THREAD_FACTORY.newThread(() -> {
MessageProcessor p = new MessageProcessor(client, Math.max(1L, Math.round(tcptimeout)), serverKey, processorDispatcher, readBufferSize);
try {
while (client.isOpen() && client.isConnected()) {
@@ -202,7 +207,7 @@ private void register(ServerSocketChannel serverSocket)
p.shutdown();
}
- });
+ }).start();
}
private static boolean parseDisconnectEvents(IOException e) {
@@ -234,6 +239,7 @@ private Map buildProcessorOptions() {
options.put("recceiver.cf.service-url", cfServiceUrl);
options.put("recceiver.cf.username", cfUsername);
options.put("recceiver.cf.password", cfPassword);
+ options.put("recceiver.cf.http-timeout", cfHttpTimeout);
options.put("recceiver.cf.delete-on-clean", cfDeleteOnClean);
options.put("recceiver.cf.clean-on-startup", cfCleanOnStartup);
options.put("recceiver.cf.delete-on-shutdown", cfDeleteOnShutdown);
diff --git a/src/main/java/org/phoebus/recceiver/spi/RecSyncProcessorRegistry.java b/src/main/java/org/phoebus/recceiver/spi/RecSyncProcessorRegistry.java
index e4305bc..b0473d2 100644
--- a/src/main/java/org/phoebus/recceiver/spi/RecSyncProcessorRegistry.java
+++ b/src/main/java/org/phoebus/recceiver/spi/RecSyncProcessorRegistry.java
@@ -19,6 +19,7 @@ public static RecSyncProcessorDispatcher loadDispatcher(Map opti
ServiceLoader directLoader = ServiceLoader.load(RecSyncProcessor.class);
for (RecSyncProcessor processor : directLoader) {
+ log.info("Loading RecSyncProcessor via SPI: " + processor.getClass().getName());
try {
RecSyncProcessor initializedProcessor = processor.init(processor.name(), initOptions);
if (initializedProcessor != null) {
diff --git a/src/main/java/org/phoebus/recceiver/spi/cf/CFRecSyncProcessor.java b/src/main/java/org/phoebus/recceiver/spi/cf/CFRecSyncProcessor.java
index 8c57723..5cb3af5 100644
--- a/src/main/java/org/phoebus/recceiver/spi/cf/CFRecSyncProcessor.java
+++ b/src/main/java/org/phoebus/recceiver/spi/cf/CFRecSyncProcessor.java
@@ -7,6 +7,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
@@ -69,6 +70,7 @@
* {@code recceiver.cf.enabled} (false by default): Enable this processor.
* {@code recceiver.cf.service-url} (required if enabled): URL to the ChannelFinder service.
* {@code recceiver.cf.username} / {@code recceiver.cf.password} (optional): HTTP basic auth.
+ * {@code recceiver.cf.http-timeout} (30 by default): Base HTTP timeout in seconds for ChannelFinder requests.
* {@code recceiver.cf.alias-enabled} (false by default): Publish alias records as separate channels.
* {@code recceiver.cf.infotags} (optional): Comma/space-separated list of info tag names to
* publish as ChannelFinder properties.
@@ -86,6 +88,7 @@ public class CFRecSyncProcessor implements RecSyncProcessor {
private static final String DELETE_ON_CLEAN_KEY = "recceiver.cf.delete-on-clean";
private static final String CLEAN_ON_STARTUP_KEY = "recceiver.cf.clean-on-startup";
private static final String DELETE_ON_SHUTDOWN_KEY = "recceiver.cf.delete-on-shutdown";
+ private static final String HTTP_TIMEOUT_KEY = "recceiver.cf.http-timeout";
private static final String ALIAS_ENABLED_KEY = "recceiver.cf.alias-enabled";
private static final String INFOTAGS_KEY = "recceiver.cf.infotags";
private static final String ENVIRONMENT_VARS_KEY = "recceiver.cf.environment-vars";
@@ -134,6 +137,7 @@ public class CFRecSyncProcessor implements RecSyncProcessor {
)));
private ChannelFinderStore store;
+ private Duration httpTimeout;
private boolean deleteOnClean;
private boolean cleanOnStartup;
private boolean deleteOnShutdown;
@@ -169,6 +173,7 @@ public RecSyncProcessor init(String name, Map options) {
"recceiver.cf.username and recceiver.cf.password must both be configured when authentication is used");
}
+ this.httpTimeout = Duration.ofSeconds(Long.parseLong(options.getOrDefault(HTTP_TIMEOUT_KEY, "30").trim()));
this.store = createStore(serviceUrl, username, password);
this.deleteOnClean = Boolean.parseBoolean(options.getOrDefault(DELETE_ON_CLEAN_KEY, "false"));
this.cleanOnStartup = Boolean.parseBoolean(options.getOrDefault(CLEAN_ON_STARTUP_KEY, "false"));
@@ -186,6 +191,9 @@ public RecSyncProcessor init(String name, Map options) {
requiredProperties.addAll(environmentVarMappings.values());
this.store.ensureProperties(requiredProperties, propertyOwner);
+ log.info("CFRecSyncProcessor initialized with serviceUrl=" + serviceUrl +" httpTimeout=" + httpTimeout + " cleanOnStartup=" + cleanOnStartup + " deleteOnClean=" + deleteOnClean
+ + " deleteOnShutdown=" + deleteOnShutdown + " aliasEnabled=" + aliasEnabled
+ + " infoTagWhitelist=" + infoTagWhitelist + " environmentVarMappings=" + environmentVarMappings);
cleanupExistingActiveChannelsForRecceiver();
return this;
@@ -217,6 +225,7 @@ private void cleanupExistingActiveChannelsForRecceiver() {
// Startup cleanup mode: cleanOnStartup=true => delete, false => mark inactive.
if (cleanOnStartup) {
store.deleteChannels(activeChannels.keySet());
+ log.info("CFRecSyncProcessor startup cleanup: deleted " + activeChannels.size() + " channels for recceiverID=" + RECCEIVER_ID);
return;
}
@@ -229,12 +238,13 @@ private void cleanupExistingActiveChannelsForRecceiver() {
inactiveChannels.add(new ChannelFinderChannel(channel.name(), channel.owner(), properties));
}
store.updateChannels(inactiveChannels);
+ log.info("CFRecSyncProcessor startup cleanup: marked " + inactiveChannels.size() + " channels as Inactive for recceiverID=" + RECCEIVER_ID);
}
protected ChannelFinderStore createStore(String serviceUrl, String username, String password) {
return username == null
- ? new ChannelFinderHttpStore(serviceUrl)
- : new ChannelFinderHttpStore(serviceUrl, username, password);
+ ? new ChannelFinderHttpStore(serviceUrl, httpTimeout)
+ : new ChannelFinderHttpStore(serviceUrl, username, password, httpTimeout);
}
/**
diff --git a/src/main/java/org/phoebus/recceiver/spi/cf/ChannelFinderHttpStore.java b/src/main/java/org/phoebus/recceiver/spi/cf/ChannelFinderHttpStore.java
index 7976fd1..3a222b7 100644
--- a/src/main/java/org/phoebus/recceiver/spi/cf/ChannelFinderHttpStore.java
+++ b/src/main/java/org/phoebus/recceiver/spi/cf/ChannelFinderHttpStore.java
@@ -37,14 +37,13 @@
* GET /resources/scroll/{scrollId}: Continue a scroll query when response id is present.
* POST /resources/channels: Create or update a list of channels.
* The request body is a JSON array of channel objects.
- * DELETE /resources/channels/{name}: Delete a single channel.
+ * DELETE /resources/channels: Delete multiple channels in one request body list.
* POST /resources/properties/{name}: Create a property (if it does not exist).
*
*
*/
public class ChannelFinderHttpStore implements ChannelFinderStore {
private static final Logger log = Logger.getLogger(ChannelFinderHttpStore.class.getName());
- private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10);
private static final Duration MAX_BULK_UPDATE_TIMEOUT = Duration.ofMinutes(5);
private static final int CHANNELS_PER_TIMEOUT_STEP = 250;
private static final Duration TIMEOUT_STEP = Duration.ofSeconds(1);
@@ -53,20 +52,26 @@ public class ChannelFinderHttpStore implements ChannelFinderStore {
private final ObjectMapper objectMapper;
private final URI serviceUri;
private final String authorizationHeader;
+ private final Duration timeout;
- public ChannelFinderHttpStore(String serviceUrl) {
- this(HttpClient.newHttpClient(), defaultObjectMapper(), URI.create(serviceUrl), null);
+ public ChannelFinderHttpStore(String serviceUrl, Duration timeout) {
+ this(HttpClient.newHttpClient(), defaultObjectMapper(), URI.create(serviceUrl), null, timeout);
}
- public ChannelFinderHttpStore(String serviceUrl, String username, String password) {
- this(HttpClient.newHttpClient(), defaultObjectMapper(), URI.create(serviceUrl), basicAuth(username, password));
+ public ChannelFinderHttpStore(String serviceUrl, String username, String password, Duration timeout) {
+ this(HttpClient.newHttpClient(), defaultObjectMapper(), URI.create(serviceUrl), basicAuth(username, password), timeout);
}
- ChannelFinderHttpStore(HttpClient httpClient, ObjectMapper objectMapper, URI serviceUri, String authorizationHeader) {
+ ChannelFinderHttpStore(HttpClient httpClient, ObjectMapper objectMapper, URI serviceUri, String authorizationHeader,
+ Duration timeout) {
this.httpClient = Objects.requireNonNull(httpClient, "httpClient");
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper");
this.serviceUri = normalizeBaseUri(Objects.requireNonNull(serviceUri, "serviceUri"));
this.authorizationHeader = authorizationHeader;
+ this.timeout = Objects.requireNonNull(timeout, "timeout");
+ if (timeout.isZero() || timeout.isNegative()) {
+ throw new IllegalArgumentException("timeout must be positive: " + timeout);
+ }
}
@Override
@@ -188,10 +193,15 @@ public void updateChannels(Collection channels) {
@Override
public void deleteChannels(Collection channelNames) {
- for (String channelName : channelNames) {
- HttpRequest request = requestBuilder(channelUri(channelName)).DELETE().build();
- sendExpectingSuccess(request, "delete channel " + channelName);
+ if (channelNames.isEmpty()) {
+ return;
}
+
+ List names = new ArrayList<>(channelNames);
+ HttpRequest request = requestBuilder(channelsUri(), bulkUpdateTimeout(names.size()))
+ .method("DELETE", jsonBody(names))
+ .build();
+ sendExpectingSuccess(request, "delete channels " + names.size());
}
@Override
@@ -247,7 +257,7 @@ private HttpResponse send(HttpRequest request, String action) {
}
private HttpRequest.Builder requestBuilder(URI uri) {
- return requestBuilder(uri, DEFAULT_TIMEOUT);
+ return requestBuilder(uri, timeout);
}
private HttpRequest.Builder requestBuilder(URI uri, Duration timeout) {
@@ -261,15 +271,11 @@ private HttpRequest.Builder requestBuilder(URI uri, Duration timeout) {
return builder;
}
- // Scale POST timeout with batch size: base + 1s per 250 channels, capped to avoid unbounded waits.
- private static Duration bulkUpdateTimeout(int channelCount) {
- int safeCount = Math.max(0, channelCount);
- long extraSeconds = safeCount / CHANNELS_PER_TIMEOUT_STEP;
- Duration scaled = DEFAULT_TIMEOUT.plus(TIMEOUT_STEP.multipliedBy(extraSeconds));
- if (scaled.compareTo(MAX_BULK_UPDATE_TIMEOUT) > 0) {
- return MAX_BULK_UPDATE_TIMEOUT;
- }
- return scaled;
+ // Scale request timeout with batch size: base + 1s per 250 channels, capped to avoid unbounded waits.
+ private Duration bulkUpdateTimeout(int channelCount) {
+ long extraSeconds = Math.max(0, channelCount) / CHANNELS_PER_TIMEOUT_STEP;
+ Duration scaled = timeout.plus(TIMEOUT_STEP.multipliedBy(extraSeconds));
+ return scaled.compareTo(MAX_BULK_UPDATE_TIMEOUT) > 0 ? MAX_BULK_UPDATE_TIMEOUT : scaled;
}
private HttpRequest.BodyPublisher jsonBody(Object value) {
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b5a42bf..39d99d6 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -66,15 +66,19 @@ recceiver.cf.service-url=http://localhost:9090/ChannelFinder
recceiver.cf.username=admin
recceiver.cf.password=adminPass
+# Base HTTP timeout in seconds for ChannelFinder calls. Bulk update/delete
+# requests add 1s per 250 channels, capped at 5 minutes.
+recceiver.cf.http-timeout=30
+
# On connection close: true => delete IOC channels, false => mark IOC channels Inactive.
-recceiver.cf.delete-on-clean=false
+recceiver.cf.delete-on-clean=true
# On processor init/startup for this recceiver's currently Active channels:
# true => delete channels, false => mark channels Inactive.
-recceiver.cf.clean-on-startup=false
+recceiver.cf.clean-on-startup=true
# On application shutdown: true => delete IOC channels (default), false => mark as Inactive.
-recceiver.cf.delete-on-shutdown=false
+recceiver.cf.delete-on-shutdown=true
# Publish alias records as channels with property alias=.
recceiver.cf.alias-enabled=true
diff --git a/src/test/java/org/phoebus/recceiver/SimpleTest.java b/src/test/java/org/phoebus/recceiver/SimpleTest.java
index 25de202..501d39a 100644
--- a/src/test/java/org/phoebus/recceiver/SimpleTest.java
+++ b/src/test/java/org/phoebus/recceiver/SimpleTest.java
@@ -2,18 +2,18 @@
import org.junit.jupiter.api.Test;
-import javax.xml.bind.DatatypeConverter;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.util.HexFormat;
public class SimpleTest {
@Test
public void encoding() {
String hexString = "1234567890";
- byte[] bytes = DatatypeConverter.parseHexBinary(hexString);
+ byte[] bytes = HexFormat.of().parseHex(hexString);
- System.out.println(DatatypeConverter.printHexBinary(bytes));
+ System.out.println(HexFormat.of().formatHex(bytes));
ByteBuffer readBuffer = ByteBuffer.wrap(bytes);
ByteBuffer dstBuffer = ByteBuffer.allocate(16);
@@ -23,17 +23,17 @@ public void encoding() {
dstBuffer.put(readBuffer.array(), 0, 2);
readBuffer.position(readBuffer.position() + 2);
- System.out.println(DatatypeConverter.printHexBinary(dstBuffer.array()));
+ System.out.println(HexFormat.of().formatHex(dstBuffer.array()));
System.out.println("dst remaining: " + dstBuffer.remaining() + " dst position: " + dstBuffer.position());
System.out.println("read remaining: " + readBuffer.remaining() + " read position: " + readBuffer.position());
dstBuffer.put(readBuffer.array(), readBuffer.position(), 1);
readBuffer.position(readBuffer.position() + 1);
- System.out.println(DatatypeConverter.printHexBinary(dstBuffer.array()));
+ System.out.println(HexFormat.of().formatHex(dstBuffer.array()));
dstBuffer.put(readBuffer.array(), readBuffer.position(), 1);
readBuffer.position(readBuffer.position() + 1);
- System.out.println(DatatypeConverter.printHexBinary(dstBuffer.array()));
+ System.out.println(HexFormat.of().formatHex(dstBuffer.array()));
System.out.println("dst remaining: " + dstBuffer.remaining() + " dst position: " + dstBuffer.position());
System.out.println("read remaining: " + readBuffer.remaining() + " read position: " + readBuffer.position());
@@ -49,27 +49,27 @@ public void encoding() {
public void compact() {
String hexString = "1234567890";
- byte[] bytes = DatatypeConverter.parseHexBinary(hexString);
+ byte[] bytes = HexFormat.of().parseHex(hexString);
ByteBuffer readBuffer = ByteBuffer.allocate(5);
readBuffer.put(bytes, 0, 5);
- System.out.println(DatatypeConverter.printHexBinary(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
+ System.out.println(HexFormat.of().formatHex(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
readBuffer.flip();
- System.out.println(DatatypeConverter.printHexBinary(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
+ System.out.println(HexFormat.of().formatHex(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
readBuffer.get();
readBuffer.get();
readBuffer.get();
readBuffer.get();
- System.out.println(DatatypeConverter.printHexBinary(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
+ System.out.println(HexFormat.of().formatHex(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
readBuffer.compact();
- System.out.println(DatatypeConverter.printHexBinary(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
+ System.out.println(HexFormat.of().formatHex(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
readBuffer.put((byte) 0xFF);
readBuffer.put((byte) 0xFF);
readBuffer.put((byte) 0xFF);
readBuffer.put((byte) 0xFF);
- System.out.println(DatatypeConverter.printHexBinary(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
+ System.out.println(HexFormat.of().formatHex(readBuffer.array()) + " " + readBuffer.position() + " " + readBuffer.limit());
}