From a3e5520d3e15eb8d1cc45fab44778d77b4a056eb Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 5 Aug 2018 07:44:46 -0400 Subject: [PATCH] Resolves #372 - Icecast reconnect following network outage. Updates the lost connection timeout from default of 60 seconds down to 5 seconds so that the connection state updates more quickly. Removed complex error handling and replaced with a simpler approach to catch an error and disconnect and then attempt a reconnect. Added new 'Network Unavailable' error state that detects when the network connection is unavaialable. Updated the icecast metadata updater to close the TCP session after successfully writing metadata to the socket. --- .../audio/broadcast/AudioBroadcaster.java | 6 +- .../audio/broadcast/BroadcastState.java | 5 + .../audio/broadcast/BroadcastStatusPanel.java | 12 +- .../IcecastBroadcastMetadataUpdater.java | 92 ++++++++-------- .../icecast/IcecastTCPAudioBroadcaster.java | 104 ++++++------------ 5 files changed, 94 insertions(+), 125 deletions(-) diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java index 413bbdf1..876e2ea9 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java @@ -245,7 +245,11 @@ public abstract class AudioBroadcaster implements Listener { if(mBroadcastState != state) { - mLog.info("[" + getStreamName() + "] status: " + state); + if(state == BroadcastState.CONNECTED || state == BroadcastState.DISCONNECTED) + { + mLog.info("[" + getStreamName() + "] status: " + state); + } + mBroadcastState = state; broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE)); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastState.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastState.java index 5fd6c3f3..9aecadbd 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastState.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastState.java @@ -70,6 +70,11 @@ public enum BroadcastState */ MOUNT_POINT_IN_USE("Mount Point In Use", false), + /** + * Network is unavailable or the server address cannot be resolved + */ + NETWORK_UNAVAILABLE("Network Unavailable", false), + /** * Server is not known or reachable */ diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastStatusPanel.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastStatusPanel.java index b82e31d2..38ce234e 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastStatusPanel.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastStatusPanel.java @@ -20,9 +20,14 @@ package io.github.dsheirer.audio.broadcast; import net.miginfocom.swing.MigLayout; -import javax.swing.*; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingConstants; import javax.swing.table.DefaultTableCellRenderer; -import java.awt.*; +import java.awt.Color; +import java.awt.Component; public class BroadcastStatusPanel extends JPanel { @@ -96,7 +101,8 @@ public class BroadcastStatusPanel extends JPanel setBackground(table.getBackground()); setForeground(Color.LIGHT_GRAY); } - else if(state == BroadcastState.INVALID_SETTINGS) + else if(state == BroadcastState.INVALID_SETTINGS || + state == BroadcastState.NETWORK_UNAVAILABLE) { setBackground(Color.YELLOW); setForeground(table.getForeground()); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java index 1bd767f8..924fd8ec 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java @@ -22,6 +22,7 @@ import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater; import io.github.dsheirer.channel.metadata.Metadata; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.util.ThreadPool; +import org.apache.mina.core.RuntimeIoException; import org.apache.mina.core.future.ConnectFuture; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; @@ -35,7 +36,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; -import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.URLEncoder; import java.nio.channels.UnresolvedAddressException; @@ -117,70 +117,64 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate { mMetadataQueue.offer(getSong(metadata)); - if(mUpdating.compareAndSet(false, true)) + if(!mUpdating.get() && !mMetadataQueue.isEmpty()) { - String song = mMetadataQueue.poll(); - - while(song != null) + Runnable runnable = new Runnable() { - HttpRequest updateRequest = createUpdateRequest(song); - - if(updateRequest != null) + @Override + public void run() { - ThreadPool.SCHEDULED.schedule(new Runnable() + if(!mMetadataQueue.isEmpty() && mUpdating.compareAndSet(false, true)) { - @Override - public void run() + try { - try - { - ConnectFuture connectFuture = getSocketConnector() - .connect(new InetSocketAddress(mIcecastConfiguration.getHost(), - mIcecastConfiguration.getPort())); - connectFuture.awaitUninterruptibly(); - IoSession session = connectFuture.getSession(); + ConnectFuture connectFuture = getSocketConnector() + .connect(new InetSocketAddress(mIcecastConfiguration.getHost(), + mIcecastConfiguration.getPort())); + connectFuture.awaitUninterruptibly(); + IoSession session = connectFuture.getSession(); - if(session != null) - { - session.write(updateRequest); - } - } - catch(UnresolvedAddressException uae) + if(session != null) { - //Do nothing - the server is temporarily unavailable - } - catch(Exception e) - { - Throwable throwableCause = e.getCause(); + String song = mMetadataQueue.poll(); - if(throwableCause instanceof ConnectException) + while(song != null) { - //Do nothing, the server is unavailable - } - else if(throwableCause instanceof UnresolvedAddressException) - { - //Do nothing - the server is temporarily unavailable - } - else - { - if(!mStackTraceLoggingSuppressed) + HttpRequest updateRequest = createUpdateRequest(song); + + if(updateRequest != null) { - mLog.error("Error sending metadata update. Future errors will " + - "be suppressed", e); - - mStackTraceLoggingSuppressed = true; + session.write(updateRequest); } + + //Fetch next metadata update to send + song = mMetadataQueue.poll(); } + + session.closeNow(); } } - }, 0l, TimeUnit.SECONDS); + catch(UnresolvedAddressException | RuntimeIoException ure) + { + //Do nothing - the server is temporarily unavailable + } + catch(Exception e) + { + Throwable throwableCause = e.getCause(); + + if(!mStackTraceLoggingSuppressed) + { + mLog.error("Error sending metadata update. Future errors will be suppressed", e); + mStackTraceLoggingSuppressed = true; + } + } + + mUpdating.set(false); + } } + }; - //Fetch next metadata update to send - song = mMetadataQueue.poll(); - } - - mUpdating.set(false); + ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS); } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java index dc586f1f..8618d6b2 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java @@ -27,15 +27,14 @@ import org.apache.mina.core.RuntimeIoException; import org.apache.mina.core.future.ConnectFuture; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; -import org.apache.mina.core.write.WriteTimeoutException; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.net.ConnectException; import java.net.InetSocketAddress; +import java.net.SocketException; import java.nio.channels.UnresolvedAddressException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -46,7 +45,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster private final static String TERMINATOR = "\r\n"; private final static String SEPARATOR = ":"; private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds - private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds + private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds + private static final int WRITE_TIMEOUT_SECONDS = 5; private NioSocketConnector mSocketConnector; private IoSession mStreamingSession = null; @@ -101,7 +101,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster if(mSocketConnector == null) { mSocketConnector = new NioSocketConnector(); - mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT); + mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS); // LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class); // loggingFilter.setMessageSentLogLevel(LogLevel.NONE); @@ -127,45 +127,41 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), getBroadcastConfiguration().getPort())); - boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS); + boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); - mStreamingSession = future.getSession(); + if(connected) + { + mStreamingSession = future.getSession(); + mConnecting.set(false); + return; + } + } + catch(RuntimeIoException rioe) + { + if(rioe.getCause() instanceof SocketException) + { + setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE); + mConnecting.set(false); + return; + } } catch(UnresolvedAddressException uae) { - //What do you do with this? Nothing. + setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE); + mConnecting.set(false); + return; } - catch(InterruptedException ie) + catch(Exception e) { - //We couldn't connect within the timeout period -- disconnect so that we can try again later - disconnect(); - } - catch(RuntimeIoException rie) - { - Throwable throwableCause = rie.getCause(); - - if(throwableCause instanceof ConnectException) - { - setBroadcastState(BroadcastState.NO_SERVER); - } - else if(throwableCause != null) - { - setBroadcastState(BroadcastState.ERROR); - mLog.debug("[" + getStreamName() + "] failed to connect", rie); - } - else - { - setBroadcastState(BroadcastState.ERROR); - mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available"); - } - - disconnect(); + mLog.error("Error", e); + //Disregard ... we'll disconnect and try again } catch(Throwable t) { - disconnect(); + mLog.error("Throwable error caught", t); } + disconnect(); mConnecting.set(false); } }; @@ -188,6 +184,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster } else { + setBroadcastState(BroadcastState.DISCONNECTED); mLastConnectionAttempt = System.currentTimeMillis(); } } @@ -263,49 +260,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { - if(cause instanceof IOException) - { - IOException ioe = (IOException)cause; - - if(ioe.getMessage() != null) - { - String reason = ioe.getMessage(); - - if(connected()) - { - disconnect(); - } - else if(reason.startsWith("Connection reset")) - { - disconnect(); - } - else if(reason.startsWith("Operation timed out")) - { - disconnect(); - } - else - { - setBroadcastState(BroadcastState.ERROR); - disconnect(); - } - } - else if(ioe instanceof WriteTimeoutException) - { - disconnect(); - } - else - { - setBroadcastState(BroadcastState.ERROR); - disconnect(); - mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause); - } - } - else + if(!(cause instanceof IOException)) { mLog.error("[" + getStreamName() + "] Broadcast error", cause); - setBroadcastState(BroadcastState.ERROR); - disconnect(); } + + disconnect(); } @Override