/*
* StatusChecker.java February 2014
*
* Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net>
*
* 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.simpleframework.http.socket.service;
import static org.simpleframework.http.socket.CloseCode.INTERNAL_SERVER_ERROR;
import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE;
import static org.simpleframework.http.socket.FrameType.PING;
import static org.simpleframework.http.socket.service.ServiceEvent.ERROR;
import static org.simpleframework.http.socket.service.ServiceEvent.PING_EXPIRED;
import static org.simpleframework.http.socket.service.ServiceEvent.PONG_RECEIVED;
import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_PING;
import java.util.concurrent.atomic.AtomicLong;
import org.simpleframework.common.thread.Scheduler;
import org.simpleframework.http.Request;
import org.simpleframework.http.socket.DataFrame;
import org.simpleframework.http.socket.Frame;
import org.simpleframework.http.socket.Reason;
import org.simpleframework.transport.Channel;
import org.simpleframework.transport.trace.Trace;
/**
* The <code>StatusChecker</code> object is used to perform health
* checks on connected sessions. Health is determined using the ping
* pong protocol defined in RFC 6455. The ping pong protocol requires
* that any endpoint must respond to a ping control frame with a pong
* control frame containing the same payload. This session checker
* will send out out ping controls frames and wait for a pong frame.
* If it does not receive a pong frame after a configured expiry time
* then it will close the associated session.
*
* @author Niall Gallagher
*/
class StatusChecker implements Runnable{
/**
* This is used to perform the monitoring of the sessions.
*/
private final StatusResultListener listener;
/**
* This is the WebSocket this this pinger will be monitoring.
*/
private final FrameConnection connection;
/**
* This is the shared scheduler used to execute this checker.
*/
private final Scheduler scheduler;
/**
* This is a count of the number of unacknowledged ping frames.
*/
private final AtomicLong counter;
/**
* This is the underling TCP channel that is being checked.
*/
private final Channel channel;
/**
* The only reason for a close is for an unexpected error.
*/
private final Reason normal;
/**
* The only reason for a close is for an unexpected error.
*/
private final Reason error;
/**
* This is used to trace various events for this pinger.
*/
private final Trace trace;
/**
* This is the frame that contains the ping to send.
*/
private final Frame frame;
/**
* This is the frequency with which the checker should run.
*/
private final long frequency;
/**
* Constructor for the <code>StatusChecker</code> object. This
* is used to create a pinger that will send out ping frames at
* a specified interval. If a session does not respond within
* three times the duration of the ping the connection is reset.
*
* @param connection this is the WebSocket to send the frames
* @param request this is the associated request
* @param scheduler this is the scheduler used to execute this
* @param frequency this is the frequency with which to ping
*/
public StatusChecker(FrameConnection connection, Request request, Scheduler scheduler, long frequency) {
this.listener = new StatusResultListener(this);
this.error = new Reason(INTERNAL_SERVER_ERROR);
this.normal = new Reason(NORMAL_CLOSURE);
this.frame = new DataFrame(PING);
this.counter = new AtomicLong();
this.channel = request.getChannel();
this.trace = channel.getTrace();
this.connection = connection;
this.scheduler = scheduler;
this.frequency = frequency;
}
/**
* This is used to kick of the status checking. Here an initial
* ping is sent over the socket and the task is then scheduled to
* check the result after the frequency period has expired. If
* this method fails for any reason the TCP channel is closed.
*/
public void start() {
try {
connection.register(listener);
trace.trace(WRITE_PING);
connection.send(frame);
counter.getAndIncrement();
scheduler.execute(this, frequency);
} catch(Exception cause) {
trace.trace(ERROR, cause);
channel.close();
}
}
/**
* This method is used to check to see if a session has expired.
* If there have been three unacknowledged ping events then this
* will force a closure of the WebSocket connection. This is done
* to ensure only healthy connections are maintained within the
* server, also RFC 6455 recommends using the ping pong protocol.
*/
public void run() {
long count = counter.get();
try {
if(count < 3) {
trace.trace(WRITE_PING);
connection.send(frame);
counter.getAndIncrement();
scheduler.execute(this, frequency); // schedule the next one
} else {
trace.trace(PING_EXPIRED);
connection.close(normal);
}
} catch (Exception cause) {
trace.trace(ERROR, cause);
channel.close();
}
}
/**
* If the connection gets a response to its ping message then this
* will reset the internal counter. This ensure that the connection
* does not time out. If after three pings there is not response
* from the other side then the connection will be terminated.
*/
public void refresh() {
try {
trace.trace(PONG_RECEIVED);
counter.set(0);
} catch(Exception cause) {
trace.trace(ERROR, cause);
channel.close();
}
}
/**
* This is used to close the session and send a 1011 close code
* to the client indicating an internal server error. Closing
* of the session in this manner only occurs if there is an
* expiry of the session or an I/O error, both of which are
* unexpected and violate the behaviour as defined in RFC 6455.
*/
public void failure() {
try {
connection.close(error);
channel.close();
} catch(Exception cause) {
trace.trace(ERROR, cause);
channel.close();
}
}
/**
* This is used to close the session and send a 1000 close code
* to the client indicating a normal closure. This will be called
* when there is a close notification dispatched to the status
* listener. Typically here a graceful closure is best.
*/
public void close() {
try {
connection.close(normal);
channel.close();
} catch(Exception cause) {
trace.trace(ERROR, cause);
channel.close();
}
}
}