44import java .net .http .HttpClient ;
55import java .net .http .WebSocket ;
66import java .net .http .WebSocket .Listener ;
7+ import java .nio .ByteBuffer ;
78import java .util .concurrent .CompletionStage ;
9+ import java .util .concurrent .Executors ;
10+ import java .util .concurrent .ScheduledExecutorService ;
811import java .util .concurrent .TimeUnit ;
12+ import java .util .concurrent .ConcurrentLinkedQueue ;
13+ import java .util .concurrent .atomic .AtomicBoolean ;
914
1015import org .apache .log4j .Level ;
1116import org .apache .log4j .Logger ;
@@ -25,6 +30,10 @@ public class OTPListener {
2530
2631 public static volatile boolean bTerminate = false ;
2732
33+ private static final AtomicBoolean reconnecting = new AtomicBoolean (false );
34+
35+ public static final ConcurrentLinkedQueue <String > reconnectEvents = new ConcurrentLinkedQueue <>();
36+
2837 public OTPListener () {
2938 if (ConfigManager .IsDebugEnabled ()) {
3039 logger .setLevel (Level .ALL );
@@ -75,13 +84,36 @@ private static class WebSocketClient implements WebSocket.Listener {
7584
7685 private final ObjectMapper objectMapper = new ObjectMapper ();
7786 private final StringBuilder messageBuffer = new StringBuilder ();
87+ private ScheduledExecutorService pingScheduler ;
7888
7989 @ Override
8090 public void onOpen (WebSocket webSocket ) {
8191 logger .info ("OTP WebSocket connection opened." );
92+ startPing (webSocket );
8293 Listener .super .onOpen (webSocket );
8394 }
8495
96+ private void startPing (WebSocket webSocket ) {
97+ pingScheduler = Executors .newSingleThreadScheduledExecutor (r -> {
98+ Thread t = new Thread (r , "ws-ping" );
99+ t .setDaemon (true );
100+ return t ;
101+ });
102+ pingScheduler .scheduleAtFixedRate (() -> {
103+ if (bTerminate ) {
104+ pingScheduler .shutdown ();
105+ return ;
106+ }
107+ webSocket .sendPing (ByteBuffer .wrap ("ping" .getBytes ()));
108+ }, 30 , 30 , TimeUnit .SECONDS );
109+ }
110+
111+ private void stopPing () {
112+ if (pingScheduler != null && !pingScheduler .isShutdown ()) {
113+ pingScheduler .shutdownNow ();
114+ }
115+ }
116+
85117 @ Override
86118 public synchronized CompletionStage <?> onText (WebSocket webSocket , CharSequence data , boolean last ) {
87119
@@ -172,13 +204,70 @@ public synchronized CompletionStage<?> onText(WebSocket webSocket, CharSequence
172204 @ Override
173205 public void onError (WebSocket webSocket , Throwable error ) {
174206 logger .error ("WebSocket error occurred" , error );
207+ stopPing ();
208+ scheduleReconnect ("error" );
175209 Listener .super .onError (webSocket , error );
176210 }
177211
178212 @ Override
179213 public CompletionStage <?> onClose (WebSocket webSocket , int statusCode , String reason ) {
180214 logger .warn ("WebSocket closed. Status: " + statusCode + " Reason: " + reason );
215+ stopPing ();
216+ if (statusCode != 1000 ) {
217+ scheduleReconnect ("abnormal close (status=" + statusCode + ")" );
218+ }
181219 return Listener .super .onClose (webSocket , statusCode , reason );
182220 }
221+
222+ private void scheduleReconnect (String trigger ) {
223+ if (bTerminate ) return ;
224+ if (!reconnecting .compareAndSet (false , true )) {
225+ logger .info ("Reconnect already in progress, ignoring trigger: " + trigger );
226+ return ;
227+ }
228+ logger .info ("Scheduling WebSocket reconnect after " + trigger );
229+ Thread t = new Thread (() -> {
230+ try {
231+ int attempt = 0 ;
232+ while (!bTerminate ) {
233+ attempt ++;
234+ long delayMs = Math .min (3000L * attempt , 60000L );
235+ try {
236+ Thread .sleep (delayMs );
237+ } catch (InterruptedException e ) {
238+ Thread .currentThread ().interrupt ();
239+ return ;
240+ }
241+ if (bTerminate ) return ;
242+ try {
243+ URI iamUri = URI .create (ConfigManager .getIAMUrl ());
244+ String host = iamUri .getHost ();
245+ int firstDot = host .indexOf ('.' );
246+ if (firstDot < 0 || firstDot == host .length () - 1 ) return ;
247+ String domain = host .substring (firstDot + 1 );
248+ String websocketUrl = "wss://smtp." + domain + "/mocksmtp/websocket" ;
249+ logger .info ("Reconnecting WebSocket (attempt " + attempt + "): " + websocketUrl );
250+ HTTP_CLIENT .newWebSocketBuilder ()
251+ .buildAsync (URI .create (websocketUrl ), new WebSocketClient ())
252+ .get (30 , TimeUnit .SECONDS );
253+ logger .info ("WebSocket reconnected successfully on attempt " + attempt );
254+ reconnectEvents .add ("WebSocket reconnected successfully on attempt "
255+ + attempt + " at " + java .time .LocalDateTime .now ());
256+ return ;
257+ } catch (Exception e ) {
258+ String failMsg = "WebSocket reconnect attempt " + attempt + " failed: "
259+ + e .getMessage () + " at " + java .time .LocalDateTime .now ();
260+ logger .warn (failMsg + ". Retrying in "
261+ + Math .min (3000L * (attempt + 1 ), 60000L ) + "ms..." );
262+ reconnectEvents .add (failMsg );
263+ }
264+ }
265+ } finally {
266+ reconnecting .set (false );
267+ }
268+ }, "ws-reconnect" );
269+ t .setDaemon (true );
270+ t .start ();
271+ }
183272 }
184273}
0 commit comments