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(getClass(), "waitingForResponse");
143
144 private final AttributeKey IGNORE_READER_IDLE_ONCE = new AttributeKey(getClass(), "ignoreReaderIdleOnce");
145
146 private final KeepAliveMessageFactory messageFactory;
147
148 private final IdleStatus interestedIdleStatus;
149
150 private volatile KeepAliveRequestTimeoutHandler requestTimeoutHandler;
151
152 private volatile int requestInterval;
153
154 private volatile int requestTimeout;
155
156 private volatile boolean forwardEvent;
157
158 /**
159 * Creates a new instance with the default properties.
160 * The default property values are:
161 * <ul>
162 * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
163 * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
164 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
165 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
166 * </ul>
167 */
168 public KeepAliveFilter(KeepAliveMessageFactory messageFactory) {
169 this(messageFactory, IdleStatus.READER_IDLE, KeepAliveRequestTimeoutHandler.CLOSE);
170 }
171
172 /**
173 * Creates a new instance with the default properties.
174 * The default property values are:
175 * <ul>
176 * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
177 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
178 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
179 * </ul>
180 */
181 public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus) {
182 this(messageFactory, interestedIdleStatus, KeepAliveRequestTimeoutHandler.CLOSE, 60, 30);
183 }
184
185 /**
186 * Creates a new instance with the default properties.
187 * The default property values are:
188 * <ul>
189 * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
190 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
191 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
192 * </ul>
193 */
194 public KeepAliveFilter(KeepAliveMessageFactory messageFactory, KeepAliveRequestTimeoutHandler policy) {
195 this(messageFactory, IdleStatus.READER_IDLE, policy, 60, 30);
196 }
197
198 /**
199 * Creates a new instance with the default properties.
200 * The default property values are:
201 * <ul>
202 * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
203 * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
204 * </ul>
205 */
206 public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
207 KeepAliveRequestTimeoutHandler policy) {
208 this(messageFactory, interestedIdleStatus, policy, 60, 30);
209 }
210
211 /**
212 * Creates a new instance.
213 */
214 public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
215 KeepAliveRequestTimeoutHandler policy, int keepAliveRequestInterval, int keepAliveRequestTimeout) {
216 if (messageFactory == null) {
217 throw new IllegalArgumentException("messageFactory");
218 }
219 if (interestedIdleStatus == null) {
220 throw new IllegalArgumentException("interestedIdleStatus");
221 }
222 if (policy == null) {
223 throw new IllegalArgumentException("policy");
224 }
225
226 this.messageFactory = messageFactory;
227 this.interestedIdleStatus = interestedIdleStatus;
228 requestTimeoutHandler = policy;
229
230 setRequestInterval(keepAliveRequestInterval);
231 setRequestTimeout(keepAliveRequestTimeout);
232 }
233
234 public IdleStatus getInterestedIdleStatus() {
235 return interestedIdleStatus;
236 }
237
238 public KeepAliveRequestTimeoutHandler getRequestTimeoutHandler() {
239 return requestTimeoutHandler;
240 }
241
242 public void setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler timeoutHandler) {
243 if (timeoutHandler == null) {
244 throw new IllegalArgumentException("timeoutHandler");
245 }
246 requestTimeoutHandler = timeoutHandler;
247 }
248
249 public int getRequestInterval() {
250 return requestInterval;
251 }
252
253 public void setRequestInterval(int keepAliveRequestInterval) {
254 if (keepAliveRequestInterval <= 0) {
255 throw new IllegalArgumentException("keepAliveRequestInterval must be a positive integer: "
256 + keepAliveRequestInterval);
257 }
258 requestInterval = keepAliveRequestInterval;
259 }
260
261 public int getRequestTimeout() {
262 return requestTimeout;
263 }
264
265 public void setRequestTimeout(int keepAliveRequestTimeout) {
266 if (keepAliveRequestTimeout <= 0) {
267 throw new IllegalArgumentException("keepAliveRequestTimeout must be a positive integer: "
268 + keepAliveRequestTimeout);
269 }
270 requestTimeout = keepAliveRequestTimeout;
271 }
272
273 public KeepAliveMessageFactory getMessageFactory() {
274 return messageFactory;
275 }
276
277 /**
278 * Returns <tt>true</tt> if and only if this filter forwards
279 * a {@link IoEventType#SESSION_IDLE} event to the next filter.
280 * By default, the value of this property is <tt>false</tt>.
281 */
282 public boolean isForwardEvent() {
283 return forwardEvent;
284 }
285
286 /**
287 * Sets if this filter needs to forward a
288 * {@link IoEventType#SESSION_IDLE} event to the next filter.
289 * By default, the value of this property is <tt>false</tt>.
290 */
291 public void setForwardEvent(boolean forwardEvent) {
292 this.forwardEvent = forwardEvent;
293 }
294
295 @Override
296 public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
297 if (parent.contains(this)) {
298 throw new IllegalArgumentException("You can't add the same filter instance more than once. "
299 + "Create another instance and add it.");
300 }
301 }
302
303 @Override
304 public void onPostAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
305 resetStatus(parent.getSession());
306 }
307
308 @Override
309 public void onPostRemove(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
310 resetStatus(parent.getSession());
311 }
312
313 @Override
314 public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
315 try {
316 if (messageFactory.isRequest(session, message)) {
317 Object pongMessage = messageFactory.getResponse(session, message);
318
319 if (pongMessage != null) {
320 nextFilter.filterWrite(session, new DefaultWriteRequest(pongMessage));
321 }
322 }
323
324 if (messageFactory.isResponse(session, message)) {
325 resetStatus(session);
326 }
327 } finally {
328 if (!isKeepAliveMessage(session, message)) {
329 nextFilter.messageReceived(session, message);
330 }
331 }
332 }
333
334 @Override
335 public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception {
336 Object message = writeRequest.getMessage();
337 if (!isKeepAliveMessage(session, message)) {
338 nextFilter.messageSent(session, writeRequest);
339 }
340 }
341
342 @Override
343 public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
344 if (status == interestedIdleStatus) {
345 if (!session.containsAttribute(WAITING_FOR_RESPONSE)) {
346 Object pingMessage = messageFactory.getRequest(session);
347 if (pingMessage != null) {
348 nextFilter.filterWrite(session, new DefaultWriteRequest(pingMessage));
349
350 // If policy is OFF, there's no need to wait for
351 // the response.
352 if (getRequestTimeoutHandler() != KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
353 markStatus(session);
354 if (interestedIdleStatus == IdleStatus.BOTH_IDLE) {
355 session.setAttribute(IGNORE_READER_IDLE_ONCE);
356 }
357 } else {
358 resetStatus(session);
359 }
360 }
361 } else {
362 handlePingTimeout(session);
363 }
364 } else if (status == IdleStatus.READER_IDLE) {
365 if (session.removeAttribute(IGNORE_READER_IDLE_ONCE) == null) {
366 if (session.containsAttribute(WAITING_FOR_RESPONSE)) {
367 handlePingTimeout(session);
368 }
369 }
370 }
371
372 if (forwardEvent) {
373 nextFilter.sessionIdle(session, status);
374 }
375 }
376
377 private void handlePingTimeout(IoSession session) throws Exception {
378 resetStatus(session);
379 KeepAliveRequestTimeoutHandler handler = getRequestTimeoutHandler();
380 if (handler == KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
381 return;
382 }
383
384 handler.keepAliveRequestTimedOut(this, session);
385 }
386
387 private void markStatus(IoSession session) {
388 session.getConfig().setIdleTime(interestedIdleStatus, 0);
389 session.getConfig().setReaderIdleTime(getRequestTimeout());
390 session.setAttribute(WAITING_FOR_RESPONSE);
391 }
392
393 private void resetStatus(IoSession session) {
394 session.getConfig().setReaderIdleTime(0);
395 session.getConfig().setWriterIdleTime(0);
396 session.getConfig().setIdleTime(interestedIdleStatus, getRequestInterval());
397 session.removeAttribute(WAITING_FOR_RESPONSE);
398 }
399
400 private boolean isKeepAliveMessage(IoSession session, Object message) {
401 return messageFactory.isRequest(session, message) || messageFactory.isResponse(session, message);
402 }
403 }