View Javadoc
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" summary="Message">
58   *   <tr>
59   *     <th>Name</th><th>Description</th><th>Implementation</th>
60   *   </tr>
61   *   <tr valign="top">
62   *     <td>Active</td>
63   *     <td>
64   *       You want a keep-alive request is sent when the reader is idle.
65   *       Once the request is sent, the response for the request should be
66   *       received within <tt>keepAliveRequestTimeout</tt> seconds.  Otherwise,
67   *       the specified {@link KeepAliveRequestTimeoutHandler} will be invoked.
68   *       If a keep-alive request is received, its response also should be sent back.
69   *     </td>
70   *     <td>
71   *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
72   *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
73   *       return a non-<tt>null</tt>.
74   *     </td>
75   *   </tr>
76   *   <tr valign="top">
77   *     <td>Semi-active</td>
78   *     <td>
79   *       You want a keep-alive request to be sent when the reader is idle.
80   *       However, you don't really care if the response is received or not.
81   *       If a keep-alive request is received, its response should
82   *       also be sent back.
83   *     </td>
84   *     <td>
85   *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
86   *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
87   *       return a non-<tt>null</tt>, and the <tt>timeoutHandler</tt> property
88   *       should be set to {@link KeepAliveRequestTimeoutHandler#NOOP},
89   *       {@link KeepAliveRequestTimeoutHandler#LOG} or the custom {@link KeepAliveRequestTimeoutHandler}
90   *       implementation that doesn't affect the session state nor throw an exception.
91   *     </td>
92   *   </tr>
93   *   <tr valign="top">
94   *     <td>Passive</td>
95   *     <td>
96   *       You don't want to send a keep-alive request by yourself, but the
97   *       response should be sent back if a keep-alive request is received.
98   *     </td>
99   *     <td>
100  *       {@link KeepAliveMessageFactory#getRequest(IoSession)} must return
101  *       <tt>null</tt> and {@link KeepAliveMessageFactory#getResponse(IoSession, Object)}
102  *       must return a non-<tt>null</tt>.
103  *     </td>
104  *   </tr>
105  *   <tr valign="top">
106  *     <td>Deaf Speaker</td>
107  *     <td>
108  *       You want a keep-alive request to be sent when the reader is idle, but
109  *       you don't want to send any response back.
110  *     </td>
111  *     <td>
112  *       {@link KeepAliveMessageFactory#getRequest(IoSession)} must return
113  *       a non-<tt>null</tt>,
114  *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
115  *       return <tt>null</tt> and the <tt>timeoutHandler</tt> must be set to
116  *       {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}.
117  *     </td>
118  *   </tr>
119  *   <tr valign="top">
120  *     <td>Silent Listener</td>
121  *     <td>
122  *       You don't want to send a keep-alive request by yourself nor send any
123  *       response back.
124  *     </td>
125  *     <td>
126  *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
127  *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
128  *       return <tt>null</tt>.
129  *     </td>
130  *   </tr>
131  * </table>
132  * 
133  * Please note that you must implement
134  * {@link KeepAliveMessageFactory#isRequest(IoSession, Object)} and
135  * {@link KeepAliveMessageFactory#isResponse(IoSession, Object)} properly
136  * whatever case you chose.
137  *
138  * <h2>Handling timeout</h2>
139  *
140  * {@link KeepAliveFilter} will notify its {@link KeepAliveRequestTimeoutHandler}
141  * when {@link KeepAliveFilter} didn't receive the response message for a sent
142  * keep-alive message.  The default handler is {@link KeepAliveRequestTimeoutHandler#CLOSE},
143  * but you can use other presets such as {@link KeepAliveRequestTimeoutHandler#NOOP},
144  * {@link KeepAliveRequestTimeoutHandler#LOG} or {@link KeepAliveRequestTimeoutHandler#EXCEPTION}.
145  * You can even implement your own handler.
146  *
147  * <h3>Special handler: {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}</h3>
148  *
149  * {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER} is a special handler which is
150  * dedicated for the 'deaf speaker' mode mentioned above.  Setting the
151  * <tt>timeoutHandler</tt> property to {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}
152  * stops this filter from waiting for response messages and therefore disables
153  * response timeout detection.
154  *
155  * @author <a href="http://mina.apache.org">Apache MINA Project</a>
156  * @org.apache.xbean.XBean
157  */
158 public class KeepAliveFilter extends IoFilterAdapter {
159 
160     private final AttributeKey WAITING_FOR_RESPONSE = new AttributeKey(getClass(), "waitingForResponse");
161 
162     private final AttributeKey IGNORE_READER_IDLE_ONCE = new AttributeKey(getClass(), "ignoreReaderIdleOnce");
163 
164     private final KeepAliveMessageFactory messageFactory;
165 
166     private final IdleStatus interestedIdleStatus;
167 
168     private volatile KeepAliveRequestTimeoutHandler requestTimeoutHandler;
169 
170     private volatile int requestInterval;
171 
172     private volatile int requestTimeout;
173 
174     private volatile boolean forwardEvent;
175 
176     /**
177      * Creates a new instance with the default properties.
178      * The default property values are:
179      * <ul>
180      * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
181      * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
182      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
183      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
184      * </ul>
185      * 
186      * @param messageFactory The message factory to use 
187      */
188     public KeepAliveFilter(KeepAliveMessageFactory messageFactory) {
189         this(messageFactory, IdleStatus.READER_IDLE, KeepAliveRequestTimeoutHandler.CLOSE);
190     }
191 
192     /**
193      * Creates a new instance with the default properties.
194      * The default property values are:
195      * <ul>
196      * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
197      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
198      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
199      * </ul>
200      * 
201      * @param messageFactory The message factory to use 
202      * @param interestedIdleStatus The IdleStatus the filter is interested in
203      */
204     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus) {
205         this(messageFactory, interestedIdleStatus, KeepAliveRequestTimeoutHandler.CLOSE, 60, 30);
206     }
207 
208     /**
209      * Creates a new instance with the default properties.
210      * The default property values are:
211      * <ul>
212      * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
213      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
214      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
215      * </ul>
216      * 
217      * @param messageFactory The message factory to use 
218      * @param policy The TimeOut handler policy
219      */
220     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, KeepAliveRequestTimeoutHandler policy) {
221         this(messageFactory, IdleStatus.READER_IDLE, policy, 60, 30);
222     }
223 
224     /**
225      * Creates a new instance with the default properties.
226      * The default property values are:
227      * <ul>
228      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
229      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
230      * </ul>
231      * 
232      * @param messageFactory The message factory to use 
233      * @param interestedIdleStatus The IdleStatus the filter is interested in
234      * @param policy The TimeOut handler policy
235      */
236     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
237             KeepAliveRequestTimeoutHandler policy) {
238         this(messageFactory, interestedIdleStatus, policy, 60, 30);
239     }
240 
241     /**
242      * Creates a new instance.
243      * 
244      * @param messageFactory The message factory to use 
245      * @param interestedIdleStatus The IdleStatus the filter is interested in
246      * @param policy The TimeOut handler policy
247      * @param keepAliveRequestInterval the interval to use
248      * @param keepAliveRequestTimeout The timeout to use
249      */
250     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
251             KeepAliveRequestTimeoutHandler policy, int keepAliveRequestInterval, int keepAliveRequestTimeout) {
252         if (messageFactory == null) {
253             throw new IllegalArgumentException("messageFactory");
254         }
255         
256         if (interestedIdleStatus == null) {
257             throw new IllegalArgumentException("interestedIdleStatus");
258         }
259         
260         if (policy == null) {
261             throw new IllegalArgumentException("policy");
262         }
263 
264         this.messageFactory = messageFactory;
265         this.interestedIdleStatus = interestedIdleStatus;
266         requestTimeoutHandler = policy;
267 
268         setRequestInterval(keepAliveRequestInterval);
269         setRequestTimeout(keepAliveRequestTimeout);
270     }
271 
272     /**
273      * @return The {@link IdleStatus} 
274      */
275     public IdleStatus getInterestedIdleStatus() {
276         return interestedIdleStatus;
277     }
278 
279     /**
280      * @return The timeout request handler
281      */
282     public KeepAliveRequestTimeoutHandler getRequestTimeoutHandler() {
283         return requestTimeoutHandler;
284     }
285 
286     /**
287      * Set the timeout handler
288      * 
289      * @param timeoutHandler The instance of {@link KeepAliveRequestTimeoutHandler} to use
290      */
291     public void setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler timeoutHandler) {
292         if (timeoutHandler == null) {
293             throw new IllegalArgumentException("timeoutHandler");
294         }
295         requestTimeoutHandler = timeoutHandler;
296     }
297 
298     /**
299      * @return the interval for keep alive messages
300      */
301     public int getRequestInterval() {
302         return requestInterval;
303     }
304 
305     /**
306      * Sets the interval for keepAlive messages
307      * 
308      * @param keepAliveRequestInterval the interval to set
309      */
310     public void setRequestInterval(int keepAliveRequestInterval) {
311         if (keepAliveRequestInterval <= 0) {
312             throw new IllegalArgumentException("keepAliveRequestInterval must be a positive integer: "
313                     + keepAliveRequestInterval);
314         }
315         
316         requestInterval = keepAliveRequestInterval;
317     }
318 
319     /**
320      * @return The timeout
321      */
322     public int getRequestTimeout() {
323         return requestTimeout;
324     }
325 
326     /**
327      * Sets the timeout
328      * 
329      * @param keepAliveRequestTimeout The timeout to set
330      */
331     public void setRequestTimeout(int keepAliveRequestTimeout) {
332         if (keepAliveRequestTimeout <= 0) {
333             throw new IllegalArgumentException("keepAliveRequestTimeout must be a positive integer: "
334                     + keepAliveRequestTimeout);
335         }
336         
337         requestTimeout = keepAliveRequestTimeout;
338     }
339 
340     /**
341      * @return The message factory
342      */
343     public KeepAliveMessageFactory getMessageFactory() {
344         return messageFactory;
345     }
346 
347     /**
348      * @return <tt>true</tt> if and only if this filter forwards
349      * a {@link IoEventType#SESSION_IDLE} event to the next filter.
350      * By default, the value of this property is <tt>false</tt>.
351      */
352     public boolean isForwardEvent() {
353         return forwardEvent;
354     }
355 
356     /**
357      * Sets if this filter needs to forward a
358      * {@link IoEventType#SESSION_IDLE} event to the next filter.
359      * By default, the value of this property is <tt>false</tt>.
360      * 
361      * @param forwardEvent a flag set to tell if the filter has to forward a {@link IoEventType#SESSION_IDLE} event
362      */
363     public void setForwardEvent(boolean forwardEvent) {
364         this.forwardEvent = forwardEvent;
365     }
366 
367     /**
368      * {@inheritDoc}
369      */
370     @Override
371     public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
372         if (parent.contains(this)) {
373             throw new IllegalArgumentException("You can't add the same filter instance more than once. "
374                     + "Create another instance and add it.");
375         }
376     }
377 
378     /**
379      * {@inheritDoc}
380      */
381     @Override
382     public void onPostAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
383         resetStatus(parent.getSession());
384     }
385 
386     /**
387      * {@inheritDoc}
388      */
389     @Override
390     public void onPostRemove(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
391         resetStatus(parent.getSession());
392     }
393 
394     /**
395      * {@inheritDoc}
396      */
397     @Override
398     public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
399         try {
400             if (messageFactory.isRequest(session, message)) {
401                 Object pongMessage = messageFactory.getResponse(session, message);
402 
403                 if (pongMessage != null) {
404                     nextFilter.filterWrite(session, new DefaultWriteRequest(pongMessage));
405                 }
406             }
407 
408             if (messageFactory.isResponse(session, message)) {
409                 resetStatus(session);
410             }
411         } finally {
412             if (!isKeepAliveMessage(session, message)) {
413                 nextFilter.messageReceived(session, message);
414             }
415         }
416     }
417 
418     /**
419      * {@inheritDoc}
420      */
421     @Override
422     public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception {
423         Object message = writeRequest.getMessage();
424         
425         if (!isKeepAliveMessage(session, message)) {
426             nextFilter.messageSent(session, writeRequest);
427         }
428     }
429 
430     /**
431      * {@inheritDoc}
432      */
433     @Override
434     public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
435         if (status == interestedIdleStatus) {
436             if (!session.containsAttribute(WAITING_FOR_RESPONSE)) {
437                 Object pingMessage = messageFactory.getRequest(session);
438                 
439                 if (pingMessage != null) {
440                     nextFilter.filterWrite(session, new DefaultWriteRequest(pingMessage));
441 
442                     // If policy is OFF, there's no need to wait for
443                     // the response.
444                     if (getRequestTimeoutHandler() != KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
445                         markStatus(session);
446                         if (interestedIdleStatus == IdleStatus.BOTH_IDLE) {
447                             session.setAttribute(IGNORE_READER_IDLE_ONCE);
448                         }
449                     } else {
450                         resetStatus(session);
451                     }
452                 }
453             } else {
454                 handlePingTimeout(session);
455             }
456         } else if (status == IdleStatus.READER_IDLE) {
457             if (session.removeAttribute(IGNORE_READER_IDLE_ONCE) == null) {
458                 if (session.containsAttribute(WAITING_FOR_RESPONSE)) {
459                     handlePingTimeout(session);
460                 }
461             }
462         }
463 
464         if (forwardEvent) {
465             nextFilter.sessionIdle(session, status);
466         }
467     }
468 
469     private void handlePingTimeout(IoSession session) throws Exception {
470         resetStatus(session);
471         KeepAliveRequestTimeoutHandler handler = getRequestTimeoutHandler();
472         if (handler == KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
473             return;
474         }
475 
476         handler.keepAliveRequestTimedOut(this, session);
477     }
478 
479     private void markStatus(IoSession session) {
480         session.getConfig().setIdleTime(interestedIdleStatus, 0);
481         session.getConfig().setReaderIdleTime(getRequestTimeout());
482         session.setAttribute(WAITING_FOR_RESPONSE);
483     }
484 
485     private void resetStatus(IoSession session) {
486         session.getConfig().setReaderIdleTime(0);
487         session.getConfig().setWriterIdleTime(0);
488         session.getConfig().setIdleTime(interestedIdleStatus, getRequestInterval());
489         session.removeAttribute(WAITING_FOR_RESPONSE);
490     }
491 
492     private boolean isKeepAliveMessage(IoSession session, Object message) {
493         return messageFactory.isRequest(session, message) || messageFactory.isResponse(session, message);
494     }
495 }