1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 *
19 */
20 package org.apache.mina.filter.keepalive;
21
22 import org.apache.mina.core.filterchain.IoFilter;
23 import org.apache.mina.core.filterchain.IoFilterAdapter;
24 import org.apache.mina.core.filterchain.IoFilterChain;
25 import org.apache.mina.core.service.IoHandler;
26 import org.apache.mina.core.session.AttributeKey;
27 import org.apache.mina.core.session.IdleStatus;
28 import org.apache.mina.core.session.IoEventType;
29 import org.apache.mina.core.session.IoSession;
30 import org.apache.mina.core.session.IoSessionConfig;
31 import org.apache.mina.core.write.DefaultWriteRequest;
32 import org.apache.mina.core.write.WriteRequest;
33
34 /**
35 * An {@link IoFilter} that sends a keep-alive request on
36 * {@link IoEventType#SESSION_IDLE} and sends back the response for the
37 * sent keep-alive request.
38 *
39 * <h2>Interference with {@link IoSessionConfig#setIdleTime(IdleStatus, int)}</h2>
40 *
41 * This filter adjusts <tt>idleTime</tt> of the {@link IdleStatus}s that
42 * this filter is interested in automatically (e.g. {@link IdleStatus#READER_IDLE}
43 * and {@link IdleStatus#WRITER_IDLE}.) Changing the <tt>idleTime</tt>
44 * of the {@link IdleStatus}s can lead this filter to a unexpected behavior.
45 * Please also note that any {@link IoFilter} and {@link IoHandler} behind
46 * {@link KeepAliveFilter} will not get any {@link IoEventType#SESSION_IDLE}
47 * event. To receive the internal {@link IoEventType#SESSION_IDLE} event,
48 * you can call {@link #setForwardEvent(boolean)} with <tt>true</tt>.
49 *
50 * <h2>Implementing {@link KeepAliveMessageFactory}</h2>
51 *
52 * To use this filter, you have to provide an implementation of
53 * {@link KeepAliveMessageFactory}, which determines a received or sent
54 * message is a keep-alive message or not and creates a new keep-alive
55 * message:
56 *
57 * <table border="1">
58 * <tr>
59 * <th>Name</th><th>Description</th><th>Implementation</th>
60 * </tr>
61 * <tr valign="top">
62 * <td>Active</td>
63 * <td>You want a keep-alive request is sent when the reader is idle.
64 * Once the request is sent, the response for the request should be
65 * received within <tt>keepAliveRequestTimeout</tt> seconds. Otherwise,
66 * the specified {@link KeepAliveRequestTimeoutHandler} will be invoked.
67 * If a keep-alive request is received, its response also should be sent back.
68 * </td>
69 * <td>Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
70 * {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
71 * return a non-<tt>null</tt>.</td>
72 * </tr>
73 * <tr valign="top">
74 * <td>Semi-active</td>
75 * <td>You want a keep-alive request to be sent when the reader is idle.
76 * However, you don't really care if the response is received or not.
77 * If a keep-alive request is received, its response should
78 * also be sent back.
79 * </td>
80 * <td>Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
81 * {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
82 * return a non-<tt>null</tt>, and the <tt>timeoutHandler</tt> property
83 * should be set to {@link KeepAliveRequestTimeoutHandler#NOOP},
84 * {@link KeepAliveRequestTimeoutHandler#LOG} or the custom {@link KeepAliveRequestTimeoutHandler}
85 * implementation that doesn't affect the session state nor throw an exception.
86 * </td>
87 * </tr>
88 * <tr valign="top">
89 * <td>Passive</td>
90 * <td>You don't want to send a keep-alive request by yourself, but the
91 * response should be sent back if a keep-alive request is received.</td>
92 * <td>{@link KeepAliveMessageFactory#getRequest(IoSession)} must return
93 * <tt>null</tt> and {@link KeepAliveMessageFactory#getResponse(IoSession, Object)}
94 * must return a non-<tt>null</tt>.</td>
95 * </tr>
96 * <tr valign="top">
97 * <td>Deaf Speaker</td>
98 * <td>You want a keep-alive request to be sent when the reader is idle, but
99 * you don't want to send any response back.</td>
100 * <td>{@link KeepAliveMessageFactory#getRequest(IoSession)} must return
101 * a non-<tt>null</tt>,
102 * {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
103 * return <tt>null</tt> and the <tt>timeoutHandler</tt> must be set to
104 * {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}.</td>
105 * </tr>
106 * <tr valign="top">
107 * <td>Silent Listener</td>
108 * <td>You don't want to send a keep-alive request by yourself nor send any
109 * response back.</td>
110 * <td>Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
111 * {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
112 * return <tt>null</tt>.</td>
113 * </tr>
114 * </table>
115 * Please note that you must implement
116 * {@link KeepAliveMessageFactory#isRequest(IoSession, Object)} and
117 * {@link KeepAliveMessageFactory#isResponse(IoSession, Object)} properly
118 * whatever case you chose.
119 *
120 * <h2>Handling timeout</h2>
121 *
122 * {@link KeepAliveFilter} will notify its {@link KeepAliveRequestTimeoutHandler}
123 * when {@link KeepAliveFilter} didn't receive the response message for a sent
124 * keep-alive message. The default handler is {@link KeepAliveRequestTimeoutHandler#CLOSE},
125 * but you can use other presets such as {@link KeepAliveRequestTimeoutHandler#NOOP},
126 * {@link KeepAliveRequestTimeoutHandler#LOG} or {@link KeepAliveRequestTimeoutHandler#EXCEPTION}.
127 * You can even implement your own handler.
128 *
129 * <h3>Special handler: {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}</h3>
130 *
131 * {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER} is a special handler which is
132 * dedicated for the 'deaf speaker' mode mentioned above. Setting the
133 * <tt>timeoutHandler</tt> property to {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}
134 * stops this filter from waiting for response messages and therefore disables
135 * response timeout detection.
136 *
137 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
138 * @org.apache.xbean.XBean
139 */
140 public class KeepAliveFilter extends IoFilterAdapter {
141
142 private final AttributeKey WAITING_FOR_RESPONSE = new AttributeKey(
143 getClass(), "waitingForResponse");
144 private final AttributeKey IGNORE_READER_IDLE_ONCE = new AttributeKey(
145 getClass(), "ignoreReaderIdleOnce");
146
147 private final KeepAliveMessageFactory messageFactory;
148 private final IdleStatus interestedIdleStatus;
149 private volatile KeepAliveRequestTimeoutHandler requestTimeoutHandler;
150 private volatile int requestInterval;
151 private volatile int requestTimeout;
152 private volatile boolean forwardEvent;
153
154 /**
155 * Creates a new instance with the default properties.
156 * The default property values are:
157 * <ul>
158 * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
159 * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
160 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
161 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
162 * </ul>
163 */
164 public KeepAliveFilter(KeepAliveMessageFactory messageFactory) {
165 this(messageFactory, IdleStatus.READER_IDLE, KeepAliveRequestTimeoutHandler.CLOSE);
166 }
167
168 /**
169 * Creates a new instance with the default properties.
170 * The default property values are:
171 * <ul>
172 * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
173 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
174 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
175 * </ul>
176 */
177 public KeepAliveFilter(
178 KeepAliveMessageFactory messageFactory,
179 IdleStatus interestedIdleStatus) {
180 this(messageFactory, interestedIdleStatus, KeepAliveRequestTimeoutHandler.CLOSE, 60, 30);
181 }
182
183 /**
184 * Creates a new instance with the default properties.
185 * The default property values are:
186 * <ul>
187 * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
188 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
189 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
190 * </ul>
191 */
192 public KeepAliveFilter(
193 KeepAliveMessageFactory messageFactory, KeepAliveRequestTimeoutHandler policy) {
194 this(messageFactory, IdleStatus.READER_IDLE, policy, 60, 30);
195 }
196
197 /**
198 * Creates a new instance with the default properties.
199 * The default property values are:
200 * <ul>
201 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
202 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
203 * </ul>
204 */
205 public KeepAliveFilter(
206 KeepAliveMessageFactory messageFactory,
207 IdleStatus interestedIdleStatus, KeepAliveRequestTimeoutHandler policy) {
208 this(messageFactory, interestedIdleStatus, policy, 60, 30);
209 }
210
211 /**
212 * Creates a new instance.
213 */
214 public KeepAliveFilter(
215 KeepAliveMessageFactory messageFactory,
216 IdleStatus interestedIdleStatus, KeepAliveRequestTimeoutHandler policy,
217 int keepAliveRequestInterval, int keepAliveRequestTimeout) {
218 if (messageFactory == null) {
219 throw new NullPointerException("messageFactory");
220 }
221 if (interestedIdleStatus == null) {
222 throw new NullPointerException("interestedIdleStatus");
223 }
224 if (policy == null) {
225 throw new NullPointerException("policy");
226 }
227
228 this.messageFactory = messageFactory;
229 this.interestedIdleStatus = interestedIdleStatus;
230 requestTimeoutHandler = policy;
231
232 setRequestInterval(keepAliveRequestInterval);
233 setRequestTimeout(keepAliveRequestTimeout);
234 }
235
236 public IdleStatus getInterestedIdleStatus() {
237 return interestedIdleStatus;
238 }
239
240 public KeepAliveRequestTimeoutHandler getRequestTimeoutHandler() {
241 return requestTimeoutHandler;
242 }
243
244 public void setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler timeoutHandler) {
245 if (timeoutHandler == null) {
246 throw new NullPointerException("timeoutHandler");
247 }
248 requestTimeoutHandler = timeoutHandler;
249 }
250
251 public int getRequestInterval() {
252 return requestInterval;
253 }
254
255 public void setRequestInterval(int keepAliveRequestInterval) {
256 if (keepAliveRequestInterval <= 0) {
257 throw new IllegalArgumentException(
258 "keepAliveRequestInterval must be a positive integer: " +
259 keepAliveRequestInterval);
260 }
261 requestInterval = keepAliveRequestInterval;
262 }
263
264 public int getRequestTimeout() {
265 return requestTimeout;
266 }
267
268 public void setRequestTimeout(int keepAliveRequestTimeout) {
269 if (keepAliveRequestTimeout <= 0) {
270 throw new IllegalArgumentException(
271 "keepAliveRequestTimeout must be a positive integer: " +
272 keepAliveRequestTimeout);
273 }
274 requestTimeout = keepAliveRequestTimeout;
275 }
276
277 public KeepAliveMessageFactory getMessageFactory() {
278 return messageFactory;
279 }
280
281 /**
282 * Returns <tt>true</tt> if and only if this filter forwards
283 * a {@link IoEventType#SESSION_IDLE} event to the next filter.
284 * By default, the value of this property is <tt>false</tt>.
285 */
286 public boolean isForwardEvent() {
287 return forwardEvent;
288 }
289
290 /**
291 * Sets if this filter needs to forward a
292 * {@link IoEventType#SESSION_IDLE} event to the next filter.
293 * By default, the value of this property is <tt>false</tt>.
294 */
295 public void setForwardEvent(boolean forwardEvent) {
296 this.forwardEvent = forwardEvent;
297 }
298
299 @Override
300 public void onPreAdd(IoFilterChain parent, String name,
301 NextFilter nextFilter) throws Exception {
302 if (parent.contains(this)) {
303 throw new IllegalArgumentException(
304 "You can't add the same filter instance more than once. " +
305 "Create another instance and add it.");
306 }
307 }
308
309 @Override
310 public void onPostAdd(
311 IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
312 resetStatus(parent.getSession());
313 }
314
315 @Override
316 public void onPostRemove(
317 IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
318 resetStatus(parent.getSession());
319 }
320
321 @Override
322 public void messageReceived(
323 NextFilter nextFilter, IoSession session, Object message) throws Exception {
324 try {
325 if (messageFactory.isRequest(session, message)) {
326 Object pongMessage =
327 messageFactory.getResponse(session, message);
328
329 if (pongMessage != null) {
330 nextFilter.filterWrite(
331 session, new DefaultWriteRequest(pongMessage));
332 }
333 }
334
335 if (messageFactory.isResponse(session, message)) {
336 resetStatus(session);
337 }
338 } finally {
339 if (!isKeepAliveMessage(session, message)) {
340 nextFilter.messageReceived(session, message);
341 }
342 }
343 }
344
345 @Override
346 public void messageSent(
347 NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception {
348 Object message = writeRequest.getMessage();
349 if (!isKeepAliveMessage(session, message)) {
350 nextFilter.messageSent(session, writeRequest);
351 }
352 }
353
354 @Override
355 public void sessionIdle(
356 NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
357 if (status == interestedIdleStatus) {
358 if (!session.containsAttribute(WAITING_FOR_RESPONSE)) {
359 Object pingMessage = messageFactory.getRequest(session);
360 if (pingMessage != null) {
361 nextFilter.filterWrite(
362 session,
363 new DefaultWriteRequest(pingMessage));
364
365 // If policy is OFF, there's no need to wait for
366 // the response.
367 if (getRequestTimeoutHandler() != KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
368 markStatus(session);
369 if (interestedIdleStatus == IdleStatus.BOTH_IDLE) {
370 session.setAttribute(IGNORE_READER_IDLE_ONCE);
371 }
372 } else {
373 resetStatus(session);
374 }
375 }
376 } else {
377 handlePingTimeout(session);
378 }
379 } else if (status == IdleStatus.READER_IDLE) {
380 if (session.removeAttribute(IGNORE_READER_IDLE_ONCE) == null) {
381 if (session.containsAttribute(WAITING_FOR_RESPONSE)) {
382 handlePingTimeout(session);
383 }
384 }
385 }
386
387 if (forwardEvent) {
388 nextFilter.sessionIdle(session, status);
389 }
390 }
391
392 private void handlePingTimeout(IoSession session) throws Exception {
393 resetStatus(session);
394 KeepAliveRequestTimeoutHandler handler = getRequestTimeoutHandler();
395 if (handler == KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
396 return;
397 }
398
399 handler.keepAliveRequestTimedOut(this, session);
400 }
401
402 private void markStatus(IoSession session) {
403 session.getConfig().setIdleTime(interestedIdleStatus, 0);
404 session.getConfig().setReaderIdleTime(getRequestTimeout());
405 session.setAttribute(WAITING_FOR_RESPONSE);
406 }
407
408 private void resetStatus(IoSession session) {
409 session.getConfig().setReaderIdleTime(0);
410 session.getConfig().setWriterIdleTime(0);
411 session.getConfig().setIdleTime(
412 interestedIdleStatus, getRequestInterval());
413 session.removeAttribute(WAITING_FOR_RESPONSE);
414 }
415
416 private boolean isKeepAliveMessage(IoSession session, Object message) {
417 return messageFactory.isRequest(session, message) ||
418 messageFactory.isResponse(session, message);
419 }
420 }