org.atmosphere.interceptor.HeartbeatInterceptor Maven / Gradle / Ivy
/*
* Copyright 2015 Async-IO.org
*
* 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.atmosphere.interceptor;
import org.atmosphere.HeartbeatAtmosphereResourceEvent;
import org.atmosphere.cpr.Action;
import org.atmosphere.cpr.AsyncIOInterceptorAdapter;
import org.atmosphere.cpr.AsyncIOWriter;
import org.atmosphere.cpr.AtmosphereConfig;
import org.atmosphere.cpr.AtmosphereInterceptorAdapter;
import org.atmosphere.cpr.AtmosphereInterceptorWriter;
import org.atmosphere.cpr.AtmosphereRequest;
import org.atmosphere.cpr.AtmosphereResource;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter;
import org.atmosphere.cpr.AtmosphereResourceHeartbeatEventListener;
import org.atmosphere.cpr.AtmosphereResourceImpl;
import org.atmosphere.cpr.AtmosphereResponse;
import org.atmosphere.cpr.HeaderConfig;
import org.atmosphere.util.ExecutorsFactory;
import org.atmosphere.util.IOUtils;
import org.atmosphere.util.Utils;
import com.vaadin.external.org.slf4j.Logger;
import com.vaadin.external.org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.atmosphere.cpr.ApplicationConfig.CLIENT_HEARTBEAT_INTERVAL_IN_SECONDS;
import static org.atmosphere.cpr.ApplicationConfig.HEARTBEAT_INTERVAL_IN_SECONDS;
import static org.atmosphere.cpr.ApplicationConfig.HEARTBEAT_PADDING_CHAR;
import static org.atmosphere.cpr.ApplicationConfig.RESUME_ON_HEARTBEAT;
/**
*
* An interceptor that send whitespace every in 60 seconds by default. Another value could be specified with the
* {@link org.atmosphere.cpr.ApplicationConfig#HEARTBEAT_INTERVAL_IN_SECONDS} in the atmosphere config. The heartbeat will be scheduled as soon as the
* request is suspended.
*
*
*
* Moreover, any client can ask for a particular value with the {@link HeaderConfig#X_HEARTBEAT_SERVER} header set in
* request. This value will be taken in consideration if it is greater than the configured value. Client can also
* specify the value "0" to disable heartbeat.
*
*
*
* Finally the server notifies thanks to the {@link JavaScriptProtocol} the desired heartbeat interval that the client
* should applies. This interceptor just manage the configured value and the {@link JavaScriptProtocol protocol} sends
* the value to the client.
*
*
* @author Jeanfrancois Arcand
*/
public class HeartbeatInterceptor extends AtmosphereInterceptorAdapter {
public final static String INTERCEPTOR_ADDED = HeartbeatInterceptor.class.getName();
public final static String HEARTBEAT_FUTURE = "heartbeat.future";
private static final Logger logger = LoggerFactory.getLogger(HeartbeatInterceptor.class);
private ScheduledExecutorService heartBeat;
private byte[] paddingBytes = " ".getBytes();
private boolean resumeOnHeartbeat;
private int heartbeatFrequencyInSeconds = 60;
/**
* Heartbeat from client disabled by default.
*/
private int clientHeartbeatFrequencyInSeconds = 0;
public HeartbeatInterceptor paddingText(byte[] paddingBytes) {
this.paddingBytes = paddingBytes;
return this;
}
/**
*
* Gets the bytes to use when sending an heartbeat for both client and server.
*
*
* @return the heartbeat value
*/
public byte[] getPaddingBytes() {
return this.paddingBytes;
}
public HeartbeatInterceptor heartbeatFrequencyInSeconds(int heartbeatFrequencyInSeconds) {
this.heartbeatFrequencyInSeconds = heartbeatFrequencyInSeconds;
return this;
}
public int heartbeatFrequencyInSeconds() {
return heartbeatFrequencyInSeconds;
}
/**
*
* Gets the desired heartbeat frequency from client.
*
*
* @return the frequency in seconds
*/
public int clientHeartbeatFrequencyInSeconds() {
return clientHeartbeatFrequencyInSeconds;
}
public HeartbeatInterceptor clientHeartbeatFrequencyInSeconds(int clientHeartbeatFrequencyInSeconds) {
this.clientHeartbeatFrequencyInSeconds = clientHeartbeatFrequencyInSeconds;
return this;
}
public boolean resumeOnHeartbeat() {
return resumeOnHeartbeat;
}
public HeartbeatInterceptor resumeOnHeartbeat(boolean resumeOnHeartbeat) {
this.resumeOnHeartbeat = resumeOnHeartbeat;
return this;
}
@Override
public void configure(final AtmosphereConfig config) {
// Server
String s = config.getInitParameter(HEARTBEAT_INTERVAL_IN_SECONDS);
if (s != null) {
heartbeatFrequencyInSeconds = Integer.valueOf(s);
}
// Server
s = config.getInitParameter(HEARTBEAT_PADDING_CHAR);
if (s != null) {
try {
paddingBytes = s.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("", e);
}
}
// Client
s = config.getInitParameter(CLIENT_HEARTBEAT_INTERVAL_IN_SECONDS);
if (s != null) {
clientHeartbeatFrequencyInSeconds = Integer.valueOf(s);
}
heartBeat = ExecutorsFactory.getScheduler(config);
resumeOnHeartbeat = config.getInitParameter(RESUME_ON_HEARTBEAT, true);
logger.info("HeartbeatInterceptor configured with padding value '{}', client frequency {} seconds and server frequency {} seconds", new String[]
{new String(paddingBytes), String.valueOf(heartbeatFrequencyInSeconds), String.valueOf(clientHeartbeatFrequencyInSeconds)});
}
private static class Clock extends AtmosphereResourceEventListenerAdapter implements AllowInterceptor {
public Clock() {
}
}
@Override
public Action inspect(final AtmosphereResource r) {
final AtmosphereResourceImpl impl = AtmosphereResourceImpl.class.cast(r);
final AtmosphereRequest request = impl.getRequest(false);
final AtmosphereResponse response = impl.getResponse(false);
// Check heartbeat
if (clientHeartbeatFrequencyInSeconds > 0) {
byte[] body;
try {
body = IOUtils.readEntirelyAsByte(r);
} catch (IOException e) {
logger.warn("", e);
cancelF(request);
return Action.CONTINUE;
}
if (Arrays.equals(paddingBytes, body)) {
// Dispatch an event to notify that a heartbeat has been intercepted
// TODO: see https://github.com/Atmosphere/atmosphere/issues/1561
final AtmosphereResourceEvent event = new HeartbeatAtmosphereResourceEvent(AtmosphereResourceImpl.class.cast(r));
if (AtmosphereResourceHeartbeatEventListener.class.isAssignableFrom(r.getAtmosphereHandler().getClass())) {
r.addEventListener(new AtmosphereResourceEventListenerAdapter.OnHeartbeat() {
@Override
public void onHeartbeat(AtmosphereResourceEvent event) {
AtmosphereResourceHeartbeatEventListener.class.cast(r.getAtmosphereHandler()).onHeartbeat(event);
}
});
}
// Fire event
r.notifyListeners(event);
return Action.CANCELLED;
}
request.body(body);
}
if (Utils.webSocketMessage(r)) return Action.CONTINUE;
final int interval = extractHeartbeatInterval(impl);
if (interval != 0) {
if (!Utils.pollableTransport(r.transport())) {
super.inspect(r);
final boolean wasSuspended = r.isSuspended();
// Suspended? Ok can clock now
// Otherwise, the listener will do the job
if (wasSuspended) {
clock(interval, r, request, response);
}
r.addEventListener(new Clock() {
@Override
public void onSuspend(AtmosphereResourceEvent event) {
// We did not clocked when this listener was added because connection was not already suspended
if (!wasSuspended) {
clock(interval, r, request, response);
}
}
@Override
public void onResume(AtmosphereResourceEvent event) {
cancelF(request);
}
@Override
public void onDisconnect(AtmosphereResourceEvent event) {
cancelF(request);
}
@Override
public void onClose(AtmosphereResourceEvent event) {
cancelF(request);
}
});
} else {
return Action.CONTINUE;
}
final AsyncIOWriter writer = response.getAsyncIOWriter();
if (!Utils.resumableTransport(r.transport())
&& AtmosphereInterceptorWriter.class.isAssignableFrom(writer.getClass())
&& request.getAttribute(INTERCEPTOR_ADDED) == null) {
AtmosphereInterceptorWriter.class.cast(writer).interceptor(new AsyncIOInterceptorAdapter() {
@Override
public byte[] transformPayload(AtmosphereResponse response, byte[] responseDraft, byte[] data) throws IOException {
cancelF(request);
return responseDraft;
}
@Override
public void postPayload(final AtmosphereResponse response, byte[] data, int offset, int length) {
logger.trace("Scheduling heartbeat for {}", r.uuid());
clock(interval, r, request, response);
}
});
request.setAttribute(INTERCEPTOR_ADDED, Boolean.TRUE);
}
}
return Action.CONTINUE;
}
/**
*
* Extracts the heartbeat interval as explained in class description. This method could be overridden to change the
* the configuration points.
*
*
* @param resource the resource
* @return the interval, 0 won't trigger the heartbeat
*/
protected int extractHeartbeatInterval(final AtmosphereResourceImpl resource) {
// Extract the desired heartbeat interval
// Won't be applied if lower config value
int interval = heartbeatFrequencyInSeconds;
final String s = resource.getRequest(false).getHeader(HeaderConfig.X_HEARTBEAT_SERVER);
if (s != null) {
try {
interval = Integer.parseInt(s);
if (interval != 0 && interval < heartbeatFrequencyInSeconds) {
interval = heartbeatFrequencyInSeconds;
}
} catch (NumberFormatException nfe) {
logger.warn("{} header is not an integer", HeaderConfig.X_HEARTBEAT_SERVER, nfe);
}
}
return interval;
}
void cancelF(AtmosphereRequest request) {
try {
Future> f = (Future>) request.getAttribute(HEARTBEAT_FUTURE);
if (f != null) f.cancel(false);
request.removeAttribute(HEARTBEAT_FUTURE);
} catch (Exception ex) {
// https://github.com/Atmosphere/atmosphere/issues/1503
logger.trace("", ex);
}
}
/**
*
* Configures the heartbeat sent by the server in an interval in seconds specified in parameter for the given
* resource.
*
*
* @param interval the interval in seconds
* @param r the resource
* @param request the request response
* @param response the resource response
* @return this
*/
public HeartbeatInterceptor clock(final int interval,
final AtmosphereResource r,
final AtmosphereRequest request,
final AtmosphereResponse response) {
request.setAttribute(HEARTBEAT_FUTURE, heartBeat.schedule(new Callable