Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 ./
Expand All @@ -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
Expand Down
25 changes: 8 additions & 17 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@
<version>4.7.2-SNAPSHOT</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
Comment thread
shroffk marked this conversation as resolved.
Resolved
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot-version>2.7.12</spring.boot-version>
<spring-shell.version>2.1.10</spring-shell.version>
<spring.boot-version>4.1.0</spring.boot-version>
</properties>

<licenses>
Expand Down Expand Up @@ -41,11 +40,9 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>

<dependency>
Expand All @@ -56,13 +53,6 @@
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-dependencies</artifactId>
<version>${spring-shell.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -93,9 +83,10 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<source>21</source>
<target>21</target>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/org/phoebus/recceiver/MessageProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 9 additions & 3 deletions src/main/java/org/phoebus/recceiver/RecceiverApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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()) {
Expand All @@ -202,7 +207,7 @@ private void register(ServerSocketChannel serverSocket)
p.shutdown();
}

});
}).start();
}

private static boolean parseDisconnectEvents(IOException e) {
Expand Down Expand Up @@ -234,6 +239,7 @@ private Map<String, String> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static RecSyncProcessorDispatcher loadDispatcher(Map<String, String> opti

ServiceLoader<RecSyncProcessor> 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) {
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/org/phoebus/recceiver/spi/cf/CFRecSyncProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,7 @@
* <li>{@code recceiver.cf.enabled} (false by default): Enable this processor.</li>
* <li>{@code recceiver.cf.service-url} (required if enabled): URL to the ChannelFinder service.</li>
* <li>{@code recceiver.cf.username} / {@code recceiver.cf.password} (optional): HTTP basic auth.</li>
* <li>{@code recceiver.cf.http-timeout} (30 by default): Base HTTP timeout in seconds for ChannelFinder requests.</li>
* <li>{@code recceiver.cf.alias-enabled} (false by default): Publish alias records as separate channels.</li>
* <li>{@code recceiver.cf.infotags} (optional): Comma/space-separated list of info tag names to
* publish as ChannelFinder properties.</li>
Expand All @@ -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";
Expand Down Expand Up @@ -134,6 +137,7 @@ public class CFRecSyncProcessor implements RecSyncProcessor {
)));

private ChannelFinderStore store;
private Duration httpTimeout;
private boolean deleteOnClean;
private boolean cleanOnStartup;
private boolean deleteOnShutdown;
Expand Down Expand Up @@ -169,6 +173,7 @@ public RecSyncProcessor init(String name, Map<String, String> 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"));
Expand All @@ -186,6 +191,9 @@ public RecSyncProcessor init(String name, Map<String, String> 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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@
* <li><strong>GET /resources/scroll/{scrollId}</strong>: Continue a scroll query when response id is present.</li>
* <li><strong>POST /resources/channels</strong>: Create or update a list of channels.
* The request body is a JSON array of channel objects.</li>
* <li><strong>DELETE /resources/channels/{name}</strong>: Delete a single channel.</li>
* <li><strong>DELETE /resources/channels</strong>: Delete multiple channels in one request body list.</li>
* <li><strong>POST /resources/properties/{name}</strong>: Create a property (if it does not exist).</li>
* </ul>
* </p>
*/
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);
Expand All @@ -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
Expand Down Expand Up @@ -188,10 +193,15 @@ public void updateChannels(Collection<ChannelFinderChannel> channels) {

@Override
public void deleteChannels(Collection<String> channelNames) {
for (String channelName : channelNames) {
HttpRequest request = requestBuilder(channelUri(channelName)).DELETE().build();
sendExpectingSuccess(request, "delete channel " + channelName);
if (channelNames.isEmpty()) {
return;
}

List<String> names = new ArrayList<>(channelNames);
HttpRequest request = requestBuilder(channelsUri(), bulkUpdateTimeout(names.size()))
.method("DELETE", jsonBody(names))
.build();
sendExpectingSuccess(request, "delete channels " + names.size());
}

@Override
Expand Down Expand Up @@ -247,7 +257,7 @@ private HttpResponse<String> 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) {
Expand All @@ -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) {
Expand Down
10 changes: 7 additions & 3 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=<primaryPV>.
recceiver.cf.alias-enabled=true
Expand Down
Loading
Loading