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">
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 }