/* * Copyright 2009-2010 Brian S O'Neill * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.cojen.dirmi.io; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.cojen.dirmi.ClosedException; import org.cojen.dirmi.RejectedException; import org.cojen.dirmi.RemoteTimeoutException; import org.cojen.dirmi.util.ScheduledTask; import org.cojen.dirmi.util.Timer; /** * Abstract Broker used by BasicChannelBrokerAcceptor and BasicChannelBrokerConnector. * * @author Brian S O'Neill */ abstract class BasicChannelBroker implements ChannelBroker { // How often to perform ping task. private static final int PING_DELAY_MILLIS = 2500; // How often to check if pings are being received. private static final int PING_CHECK_DELAY_MILLIS = 1000; // No ping responses after this threshold causes broker to close. private static final int PING_FAILURE_MILLIS = 10000 - PING_CHECK_DELAY_MILLIS; static final Logger cPingLogger; static { String prop = System.getProperty(BasicChannelBroker.class.getName() + ".LOG_PINGS"); if (prop == null || !prop.equalsIgnoreCase("true")) { cPingLogger = null; } else { cPingLogger = Logger.getLogger(BasicChannelBroker.class.getName()); } } protected final long mId; protected final Channel mControl; protected final CloseableGroup<Channel> mAllChannels; private final ListenerQueue<ChannelAcceptor.Listener> mListenerQueue; private final Future<?> mScheduledPingCheck; private final Future<?> mScheduledDoPing; private volatile long mLastPingNanos; BasicChannelBroker(IOExecutor executor, long id, Channel control) throws RejectedException { mId = id; mControl = control; mAllChannels = new CloseableGroup<Channel>(); mListenerQueue = new ListenerQueue<ChannelAcceptor.Listener> (executor, ChannelAcceptor.Listener.class); // Buffers only need to be large enough for command and broker id. control.setInputBufferSize(10); control.setOutputBufferSize(10); mLastPingNanos = System.nanoTime(); // Use separate tasks for checking and performing pings. If one task // was used, a hanging doPing prevents checks. PingTask pinger = new PingCheckTask(this); try { mScheduledPingCheck = executor.scheduleWithFixedDelay (pinger, PING_CHECK_DELAY_MILLIS, PING_CHECK_DELAY_MILLIS, TimeUnit.MILLISECONDS); } catch (RejectedException e) { control.disconnect(); throw e; } pinger.scheduled(mScheduledPingCheck); if (!requirePingTask()) { mScheduledDoPing = null; } else { pinger = new DoPingTask(this); try { mScheduledDoPing = executor.scheduleWithFixedDelay (pinger, PING_DELAY_MILLIS, PING_DELAY_MILLIS, TimeUnit.MILLISECONDS); } catch (RejectedException e) { mScheduledPingCheck.cancel(false); control.disconnect(); throw e; } pinger.scheduled(mScheduledDoPing); } } @Override public Object getRemoteAddress() { return mControl.getRemoteAddress(); } @Override public Object getLocalAddress() { return mControl.getLocalAddress(); } @Override public Channel connect(Timer timer) throws IOException { return connect(RemoteTimeoutException.checkRemaining(timer), timer.unit()); } @Override public Channel accept() throws IOException { ChannelAcceptWaiter listener = new ChannelAcceptWaiter(); accept(listener); return listener.waitForChannel(); } @Override public Channel accept(long timeout, TimeUnit unit) throws IOException { ChannelAcceptWaiter listener = new ChannelAcceptWaiter(); accept(listener); return listener.waitForChannel(timeout, unit); } @Override public Channel accept(Timer timer) throws IOException { return accept(RemoteTimeoutException.checkRemaining(timer), timer.unit()); } @Override public void accept(ChannelAcceptor.Listener listener) { try { mListenerQueue.enqueue(listener); } catch (RejectedException e) { mListenerQueue.dequeue().rejected(e); } } @Override public void close() { close(null); } @Override public String toString() { return "ChannelBroker {localAddress=" + getLocalAddress() + ", remoteAddress=" + getRemoteAddress() + '}'; } protected void accepted(Channel channel) { channel.register(mAllChannels); mListenerQueue.dequeue().accepted(channel); } protected void close(IOException cause) { if (mAllChannels.isClosed()) { return; } try { if (cause == null) { cause = new ClosedException(); } if (mScheduledPingCheck != null) { mScheduledPingCheck.cancel(false); } if (mScheduledDoPing != null) { mScheduledDoPing.cancel(false); } mControl.disconnect(); // Disconnect will not block trying to flush output or recycle // input. There's no point in attempting to recycle channels anyhow, // although unflushed output will get lost. Brokers are only used by // Sessions, which aren't required to flush when closed. User must // explicitly flush the Session for that behavior. mAllChannels.disconnect(); } finally { // Do last in case it blocks. mListenerQueue.dequeueForClose().closed(cause); } } /** * Call when ping received. */ protected void pinged() { mLastPingNanos = System.nanoTime(); } /** * @return true if doPing should be called periodically */ protected abstract boolean requirePingTask(); protected abstract void doPing() throws IOException; void logPingMessage(String message) { if (cPingLogger != null) { cPingLogger.info(message); } } private boolean pingCheck() { long lag = System.nanoTime() - mLastPingNanos; if (lag > PING_FAILURE_MILLIS * 1000000L) { if (cPingLogger != null) { logPingMessage("Ping missed for " + mControl + ": " + (lag / 1000000L) + " > " + PING_FAILURE_MILLIS); } close(new ClosedException("Ping failure")); return false; } else { if (cPingLogger != null && lag >= (PING_DELAY_MILLIS + PING_CHECK_DELAY_MILLIS) * 1000000L) { logPingMessage("Ping lag for " + mControl + ": " + (lag / 1000000L) + " <= " + PING_FAILURE_MILLIS); } return true; } } private static abstract class PingTask extends ScheduledTask<RuntimeException> { private final WeakReference<BasicChannelBroker> mBrokerRef; private volatile Future<?> mScheduled; PingTask(BasicChannelBroker broker) { mBrokerRef = new WeakReference<BasicChannelBroker>(broker); } protected void doRun() { BasicChannelBroker broker = mBrokerRef.get(); if (broker != null) { try { if (doTask(broker)) { return; } broker.close(new ClosedException("Ping failure")); } catch (IOException e) { broker.close(new ClosedException("Ping failure", e)); } } // Cancel ourself. Not expected to be null, so just do it. mScheduled.cancel(true); } void scheduled(Future<?> scheduled) { mScheduled = scheduled; } /** * @return false if failed */ abstract boolean doTask(BasicChannelBroker broker) throws IOException; } private static class DoPingTask extends PingTask { DoPingTask(BasicChannelBroker broker) { super(broker); } @Override boolean doTask(BasicChannelBroker broker) throws IOException { broker.doPing(); return true; } } private static class PingCheckTask extends PingTask { PingCheckTask(BasicChannelBroker broker) { super(broker); } @Override boolean doTask(BasicChannelBroker broker) { return broker.pingCheck(); } } }