4646import org .eclipse .milo .opcua .stack .core .types .builtin .NodeId ;
4747import org .eclipse .milo .opcua .stack .core .types .builtin .StatusCode ;
4848import org .eclipse .milo .opcua .stack .core .types .builtin .Variant ;
49- import org .eclipse .milo .opcua .stack .core .types .builtin .unsigned .UInteger ;
5049import org .jetbrains .annotations .NotNull ;
5150import org .jetbrains .annotations .VisibleForTesting ;
5251import org .slf4j .Logger ;
5554import java .util .List ;
5655import java .util .Map ;
5756import java .util .concurrent .CompletableFuture ;
57+ import java .util .concurrent .Executors ;
58+ import java .util .concurrent .ScheduledExecutorService ;
59+ import java .util .concurrent .ScheduledFuture ;
60+ import java .util .concurrent .TimeUnit ;
5861import java .util .concurrent .atomic .AtomicReference ;
5962import java .util .function .Function ;
6063import java .util .stream .Collectors ;
6164
6265public class OpcUaProtocolAdapter implements WritingProtocolAdapter {
6366 private static final @ NotNull Logger log = LoggerFactory .getLogger (OpcUaProtocolAdapter .class );
67+ private static final int RETRY_DELAY_SECONDS = 30 ;
6468
6569 private final @ NotNull ProtocolAdapterInformation adapterInformation ;
6670 private final @ NotNull ProtocolAdapterState protocolAdapterState ;
@@ -72,7 +76,8 @@ public class OpcUaProtocolAdapter implements WritingProtocolAdapter {
7276 private final @ NotNull DataPointFactory dataPointFactory ;
7377 private final @ NotNull ProtocolAdapterMetricsService protocolAdapterMetricsService ;
7478 private final @ NotNull OpcUaSpecificAdapterConfig config ;
75- private final @ NotNull AtomicReference <UInteger > lastSubscriptionId = new AtomicReference <>();
79+ private final @ NotNull ScheduledExecutorService retryScheduler = Executors .newSingleThreadScheduledExecutor ();
80+ private final @ NotNull AtomicReference <ScheduledFuture <?>> retryFuture = new AtomicReference <>();
7681
7782 public OpcUaProtocolAdapter (
7883 final @ NotNull ProtocolAdapterInformation adapterInformation ,
@@ -100,13 +105,13 @@ public synchronized void start(
100105 log .info ("Starting OPC UA protocol adapter {}" , adapterId );
101106 final ParsedConfig parsedConfig ;
102107 final var result = ParsedConfig .fromConfig (config );
103- if (result instanceof final Failure <ParsedConfig , String > failure ) {
104- log .error ("Failed to parse configuration for OPC UA client: {}" , failure . failure () );
105- output .failStart (new IllegalStateException (failure . failure () ),
108+ if (result instanceof Failure <ParsedConfig , String >( final String failure ) ) {
109+ log .error ("Failed to parse configuration for OPC UA client: {}" , failure );
110+ output .failStart (new IllegalStateException (failure ),
106111 "Failed to parse configuration for OPC UA client" );
107112 return ;
108- } else if (result instanceof final Success <ParsedConfig , String > success ) {
109- parsedConfig = success . result () ;
113+ } else if (result instanceof Success <ParsedConfig , String >( final ParsedConfig result1 ) ) {
114+ parsedConfig = result1 ;
110115 } else {
111116 output .failStart (new IllegalStateException ("Unexpected result type: " + result .getClass ().getName ()),
112117 "Failed to parse configuration for OPC UA client" );
@@ -121,22 +126,23 @@ public synchronized void start(
121126 dataPointFactory ,
122127 input .moduleServices ().eventService (),
123128 protocolAdapterMetricsService ,
124- config ,
125- lastSubscriptionId ))) {
129+ config ))) {
126130
127131 protocolAdapterState .setConnectionStatus (ProtocolAdapterState .ConnectionStatus .DISCONNECTED );
128- CompletableFuture .supplyAsync (() -> conn .start (parsedConfig )).whenComplete ((success , throwable ) -> {
129- if (!success || throwable != null ) {
130- this .opcUaClientConnection .set (null );
131- protocolAdapterState .setConnectionStatus (ProtocolAdapterState .ConnectionStatus .ERROR );
132- log .error ("Failed to start OPC UA client" , throwable );
133- }
134- });
135132
133+ // Attempt initial connection asynchronously
134+ attemptConnection (conn , parsedConfig , input );
135+
136+ // Adapter starts successfully even if connection isn't established yet
137+ // Hardware may come online later and automatic retry will connect
136138 log .info ("Successfully started OPC UA protocol adapter {}" , adapterId );
137139 output .startedSuccessfully ();
138140 } else {
139- log .warn ("Tried starting already started OPC UA protocol adapter {}" , adapterId );
141+ log .error ("Cannot start OPC UA protocol adapter '{}' - adapter is already started" , adapterId );
142+ output .failStart (
143+ new IllegalStateException ("Adapter already started" ),
144+ "Cannot start already started adapter. Please stop the adapter first."
145+ );
140146 }
141147 }
142148
@@ -145,6 +151,10 @@ public synchronized void stop(
145151 final @ NotNull ProtocolAdapterStopInput input ,
146152 final @ NotNull ProtocolAdapterStopOutput output ) {
147153 log .info ("Stopping OPC UA protocol adapter {}" , adapterId );
154+
155+ // Cancel any pending retries
156+ cancelRetry ();
157+
148158 final OpcUaClientConnection conn = opcUaClientConnection .getAndSet (null );
149159 if (conn != null ) {
150160 conn .stop ();
@@ -157,6 +167,21 @@ public synchronized void stop(
157167 @ Override
158168 public void destroy () {
159169 log .info ("Destroying OPC UA protocol adapter {}" , adapterId );
170+
171+ // Cancel any pending retries
172+ cancelRetry ();
173+
174+ // Shutdown retry scheduler
175+ retryScheduler .shutdown ();
176+ try {
177+ if (!retryScheduler .awaitTermination (5 , TimeUnit .SECONDS )) {
178+ retryScheduler .shutdownNow ();
179+ }
180+ } catch (final InterruptedException e ) {
181+ Thread .currentThread ().interrupt ();
182+ retryScheduler .shutdownNow ();
183+ }
184+
160185 final OpcUaClientConnection conn = opcUaClientConnection .getAndSet (null );
161186 if (conn != null ) {
162187 CompletableFuture .runAsync (() -> {
@@ -300,4 +325,89 @@ public void createTagSchema(
300325 public @ NotNull ProtocolAdapterState getProtocolAdapterState () {
301326 return protocolAdapterState ;
302327 }
328+
329+ /**
330+ * Attempts to establish connection to OPC UA server.
331+ * On failure, schedules automatic retry after RETRY_DELAY_SECONDS.
332+ */
333+ private void attemptConnection (
334+ final @ NotNull OpcUaClientConnection conn ,
335+ final @ NotNull ParsedConfig parsedConfig ,
336+ final @ NotNull ProtocolAdapterStartInput input ) {
337+
338+ CompletableFuture .supplyAsync (() -> conn .start (parsedConfig )).whenComplete ((success , throwable ) -> {
339+ if (success && throwable == null ) {
340+ // Connection succeeded - cancel any pending retries
341+ cancelRetry ();
342+ log .info ("OPC UA adapter '{}' connected successfully" , adapterId );
343+ } else {
344+ // Connection failed - clean up and schedule retry
345+ this .opcUaClientConnection .set (null );
346+ protocolAdapterState .setConnectionStatus (ProtocolAdapterState .ConnectionStatus .ERROR );
347+
348+ if (throwable != null ) {
349+ log .warn ("OPC UA adapter '{}' connection failed, will retry in {} seconds" ,
350+ adapterId , RETRY_DELAY_SECONDS , throwable );
351+ } else {
352+ log .warn ("OPC UA adapter '{}' connection returned false, will retry in {} seconds" ,
353+ adapterId , RETRY_DELAY_SECONDS );
354+ }
355+
356+ // Schedule retry attempt
357+ scheduleRetry (parsedConfig , input );
358+ }
359+ });
360+ }
361+
362+ /**
363+ * Schedules a retry attempt after RETRY_DELAY_SECONDS.
364+ */
365+ private void scheduleRetry (
366+ final @ NotNull ParsedConfig parsedConfig ,
367+ final @ NotNull ProtocolAdapterStartInput input ) {
368+
369+ final ScheduledFuture <?> future = retryScheduler .schedule (() -> {
370+ // Check if adapter was stopped before retry executes
371+ if (opcUaClientConnection .get () == null ) {
372+ log .debug ("OPC UA adapter '{}' retry cancelled - adapter was stopped" , adapterId );
373+ return ;
374+ }
375+
376+ log .info ("Retrying connection for OPC UA adapter '{}'" , adapterId );
377+
378+ // Create new connection object for retry
379+ final OpcUaClientConnection newConn = new OpcUaClientConnection (adapterId ,
380+ tagList ,
381+ protocolAdapterState ,
382+ input .moduleServices ().protocolAdapterTagStreamingService (),
383+ dataPointFactory ,
384+ input .moduleServices ().eventService (),
385+ protocolAdapterMetricsService ,
386+ config );
387+
388+ // Set as current connection and attempt
389+ if (opcUaClientConnection .compareAndSet (null , newConn )) {
390+ attemptConnection (newConn , parsedConfig , input );
391+ } else {
392+ log .debug ("OPC UA adapter '{}' retry skipped - connection already exists" , adapterId );
393+ }
394+ }, RETRY_DELAY_SECONDS , TimeUnit .SECONDS );
395+
396+ // Store future so it can be cancelled if needed
397+ final ScheduledFuture <?> oldFuture = retryFuture .getAndSet (future );
398+ if (oldFuture != null && !oldFuture .isDone ()) {
399+ oldFuture .cancel (false );
400+ }
401+ }
402+
403+ /**
404+ * Cancels any pending retry attempts.
405+ */
406+ private void cancelRetry () {
407+ final ScheduledFuture <?> future = retryFuture .getAndSet (null );
408+ if (future != null && !future .isDone ()) {
409+ future .cancel (false );
410+ log .debug ("Cancelled pending retry for OPC UA adapter '{}'" , adapterId );
411+ }
412+ }
303413}
0 commit comments