| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.ignite.cache.websession; |
| |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Map; |
| import javax.cache.CacheException; |
| import javax.cache.expiry.Duration; |
| import javax.cache.expiry.ExpiryPolicy; |
| import javax.cache.expiry.ModifiedExpiryPolicy; |
| import javax.servlet.Filter; |
| import javax.servlet.FilterChain; |
| import javax.servlet.FilterConfig; |
| import javax.servlet.ServletContext; |
| import javax.servlet.ServletException; |
| import javax.servlet.ServletRequest; |
| import javax.servlet.ServletResponse; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletRequestWrapper; |
| import javax.servlet.http.HttpSession; |
| import org.apache.ignite.Ignite; |
| import org.apache.ignite.IgniteCache; |
| import org.apache.ignite.IgniteClientDisconnectedException; |
| import org.apache.ignite.IgniteException; |
| import org.apache.ignite.IgniteLogger; |
| import org.apache.ignite.IgniteTransactions; |
| import org.apache.ignite.cluster.ClusterTopologyException; |
| import org.apache.ignite.configuration.CacheConfiguration; |
| import org.apache.ignite.internal.util.typedef.C1; |
| import org.apache.ignite.internal.util.typedef.F; |
| import org.apache.ignite.internal.util.typedef.G; |
| import org.apache.ignite.internal.util.typedef.T2; |
| import org.apache.ignite.internal.util.typedef.X; |
| import org.apache.ignite.internal.util.typedef.internal.S; |
| import org.apache.ignite.internal.util.typedef.internal.U; |
| import org.apache.ignite.internal.websession.WebSessionAttributeProcessor; |
| import org.apache.ignite.internal.websession.WebSessionEntity; |
| import org.apache.ignite.lang.IgniteClosure; |
| import org.apache.ignite.lang.IgniteFuture; |
| import org.apache.ignite.marshaller.Marshaller; |
| import org.apache.ignite.startup.servlet.ServletContextListenerStartup; |
| import org.apache.ignite.transactions.Transaction; |
| |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; |
| import static org.apache.ignite.cache.CacheAtomicityMode.ATOMIC; |
| import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL; |
| import static org.apache.ignite.cache.CacheMode.LOCAL; |
| import static org.apache.ignite.cache.CacheMode.PARTITIONED; |
| import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_ASYNC; |
| import static org.apache.ignite.transactions.TransactionConcurrency.PESSIMISTIC; |
| import static org.apache.ignite.transactions.TransactionIsolation.REPEATABLE_READ; |
| |
| /** |
| * Filter for web sessions caching. |
| * <p> |
| * This is a request filter, that you need to specify in your {@code web.xml} along |
| * with {@link ServletContextListenerStartup} to enable web sessions caching: |
| * <pre name="code" class="xml"> |
| * <listener> |
| * <listener-class>org.apache.ignite.startup.servlet.ServletContextListenerStartup</listener-class> |
| * </listener> |
| * |
| * <filter> |
| * <filter-name>WebSessionFilter</filter-name> |
| * <filter-class>org.apache.ignite.cache.websession.WebSessionFilter</filter-class> |
| * </filter> |
| * |
| * <!-- You can also specify a custom URL pattern. --> |
| * <filter-mapping> |
| * <filter-name>IgniteWebSessionsFilter</filter-name> |
| * <url-pattern>/*</url-pattern> |
| * </filter-mapping> |
| * </pre> |
| * It is also possible to specify a servlet name in a filter mapping, and a servlet URL pattern will |
| * be used in this case: |
| * <pre name="code" class="xml"> |
| * <filter> |
| * <filter-name>WebSessionFilter</filter-name> |
| * <filter-class>org.apache.ignite.cache.websession.WebSessionFilter</filter-class> |
| * </filter> |
| * |
| * <filter-mapping> |
| * <filter-name>WebSessionFilter</filter-name> |
| * <servlet-name>YourServletName</servlet-name> |
| * </filter-mapping> |
| * </pre> |
| * The filter has the following optional configuration parameters: |
| * <table class="doctable"> |
| * <tr> |
| * <th>Name</th> |
| * <th>Description</th> |
| * <th>Default</th> |
| * </tr> |
| * <tr> |
| * <td>IgniteWebSessionsGridName</td> |
| * <td>Name of the grid that contains cache for web session storage.</td> |
| * <td>{@code null} (default grid)</td> |
| * </tr> |
| * <tr> |
| * <td>IgniteWebSessionsCacheName</td> |
| * <td>Name of the cache for web session storage.</td> |
| * <td>{@code null} (default cache)</td> |
| * </tr> |
| * <tr> |
| * <td>IgniteWebSessionsMaximumRetriesOnFail</td> |
| * <td> |
| * Valid for {@code ATOMIC} caches only. Maximum number of retries for session updates in case |
| * node leaves topology and update fails. If retry is enabled, |
| * some updates can be applied more than once, otherwise some |
| * updates can be lost. |
| * <p> |
| * To disable retries, set this parameter to {@code 0}. |
| * </td> |
| * <td>{@code 3}</td> |
| * </tr> |
| * <tr> |
| * <td>IgniteWebSessionsRetriesTimeout</td> |
| * <td> |
| * Retry timeout. Related to IgniteWebSessionsMaximumRetriesOnFail param. |
| * <p> |
| * Further attempts will be cancelled in case timeout was exceeded. |
| * </td> |
| * <td>{@code 10000} (10 seconds)</td> |
| * </tr> |
| * </table> |
| * These parameters are taken from either filter init parameter list or |
| * servlet context parameters. You can specify filter init parameters as follows: |
| * <pre name="code" class="xml"> |
| * <filter> |
| * <filter-name>WebSessionFilter</filter-name> |
| * <filter-class>org.apache.ignite.cache.websession.WebSessionFilter</filter-class> |
| * <init-param> |
| * <param-name>IgniteWebSessionsGridName</param-name> |
| * <param-value>WebGrid</param-value> |
| * </init-param> |
| * <init-param> |
| * <param-name>IgniteWebSessionsCacheName</param-name> |
| * <param-value>WebCache</param-value> |
| * </init-param> |
| * |
| * <!-- Valid for ATOMIC caches only. --> |
| * <init-param> |
| * <param-name>IgniteWebSessionsMaximumRetriesOnFail</param-name> |
| * <param-value>10</param-value> |
| * </init-param> |
| * </filter> |
| * </pre> |
| * <b>Note:</b> filter init parameter has a priority over servlet context |
| * parameter; if you specify both, the servlet context parameter will be ignored. |
| * <h1 class="header">Web sessions caching and concurrent requests</h1> |
| * If your web application can accept concurrent request for one session, |
| * consider using {@link org.apache.ignite.cache.CacheAtomicityMode#TRANSACTIONAL} cache |
| * instead of {@link org.apache.ignite.cache.CacheAtomicityMode#ATOMIC}. In this case each request |
| * be processed inside pessimistic transaction which will guarantee that all |
| * updates will be applied in correct order. This is important, for example, |
| * if you get some attribute from the session, update its value and set new |
| * value back to the session. In case of {@link org.apache.ignite.cache.CacheAtomicityMode#ATOMIC} |
| * cache concurrent requests can get equal value, but {@link org.apache.ignite.cache.CacheAtomicityMode#TRANSACTIONAL} |
| * cache will always process such updates one after another. |
| */ |
| public class WebSessionFilter implements Filter { |
| /** Web sessions caching grid name parameter name. */ |
| public static final String WEB_SES_NAME_PARAM = "IgniteWebSessionsGridName"; |
| |
| /** Web sessions caching cache name parameter name. */ |
| public static final String WEB_SES_CACHE_NAME_PARAM = "IgniteWebSessionsCacheName"; |
| |
| /** Web sessions caching retry on fail parameter name (valid for ATOMIC cache only). */ |
| public static final String WEB_SES_MAX_RETRIES_ON_FAIL_NAME_PARAM = "IgniteWebSessionsMaximumRetriesOnFail"; |
| |
| /** Web sessions caching retry on fail timeout parameter name. */ |
| public static final String WEB_SES_RETRIES_TIMEOUT_NAME_PARAM = "IgniteWebSessionsRetriesTimeout"; |
| |
| /** */ |
| public static final String WEB_SES_KEEP_BINARY_PARAM = "IgniteWebSessionsKeepBinary"; |
| |
| /** Default retry on fail flag value. */ |
| public static final int DFLT_MAX_RETRIES_ON_FAIL = 3; |
| |
| /** Default retry on fail timeout flag value. */ |
| public static final int DFLT_RETRIES_ON_FAIL_TIMEOUT = 10000; |
| |
| /** Default keep binary flag. */ |
| public static final boolean DFLT_KEEP_BINARY_FLAG = true; |
| |
| /** Cache. */ |
| private IgniteCache<String, WebSession> cache; |
| |
| /** Binary cache */ |
| private IgniteCache<String, WebSessionEntity> binaryCache; |
| |
| /** Transactions. */ |
| private IgniteTransactions txs; |
| |
| /** Logger. */ |
| private IgniteLogger log; |
| |
| /** Servlet context. */ |
| private ServletContext ctx; |
| |
| /** Session ID transformer. */ |
| private IgniteClosure<String, String> sesIdTransformer; |
| |
| /** Transactions enabled flag. */ |
| private boolean txEnabled; |
| |
| /** Node. */ |
| private Ignite webSesIgnite; |
| |
| /** Cache name. */ |
| private String cacheName; |
| |
| /** */ |
| private int retries; |
| |
| /** */ |
| private int retriesTimeout; |
| |
| /** */ |
| private boolean keepBinary = DFLT_KEEP_BINARY_FLAG; |
| |
| /** */ |
| private Marshaller marshaller; |
| |
| /** {@inheritDoc} */ |
| @Override public void init(FilterConfig cfg) throws ServletException { |
| ctx = cfg.getServletContext(); |
| |
| String igniteInstanceName = U.firstNotNull( |
| cfg.getInitParameter(WEB_SES_NAME_PARAM), |
| ctx.getInitParameter(WEB_SES_NAME_PARAM)); |
| |
| cacheName = U.firstNotNull( |
| cfg.getInitParameter(WEB_SES_CACHE_NAME_PARAM), |
| ctx.getInitParameter(WEB_SES_CACHE_NAME_PARAM)); |
| |
| String retriesStr = U.firstNotNull( |
| cfg.getInitParameter(WEB_SES_MAX_RETRIES_ON_FAIL_NAME_PARAM), |
| ctx.getInitParameter(WEB_SES_MAX_RETRIES_ON_FAIL_NAME_PARAM)); |
| |
| try { |
| retries = retriesStr != null ? Integer.parseInt(retriesStr) : DFLT_MAX_RETRIES_ON_FAIL; |
| } |
| catch (NumberFormatException e) { |
| throw new IgniteException("Maximum number of retries parameter is invalid: " + retriesStr, e); |
| } |
| |
| String retriesTimeoutStr = U.firstNotNull( |
| cfg.getInitParameter(WEB_SES_RETRIES_TIMEOUT_NAME_PARAM), |
| ctx.getInitParameter(WEB_SES_RETRIES_TIMEOUT_NAME_PARAM)); |
| |
| try { |
| retriesTimeout = retriesTimeoutStr != null ? |
| Integer.parseInt(retriesTimeoutStr) : DFLT_RETRIES_ON_FAIL_TIMEOUT; |
| } |
| catch (NumberFormatException e) { |
| throw new IgniteException("Retries timeout parameter is invalid: " + retriesTimeoutStr, e); |
| } |
| |
| final String binParam = cfg.getInitParameter(WEB_SES_KEEP_BINARY_PARAM); |
| |
| if (!F.isEmpty(binParam)) |
| keepBinary = Boolean.parseBoolean(binParam); |
| |
| webSesIgnite = G.ignite(igniteInstanceName); |
| |
| if (webSesIgnite == null) |
| throw new IgniteException("Ignite instance for web sessions caching is not started (is it configured?): " + |
| igniteInstanceName); |
| |
| txs = webSesIgnite.transactions(); |
| |
| log = webSesIgnite.log(); |
| |
| marshaller = webSesIgnite.configuration().getMarshaller(); |
| |
| initCache(); |
| |
| String srvInfo = ctx.getServerInfo(); |
| |
| // Special case for WebLogic, which appends timestamps to session |
| // IDs upon session creation (the created session ID looks like: |
| // pdpTSTcCcG6CVM8BTZWzxjTB1lh3w7zFbYVvwBb4bJGjrBx3TMPl!-508312620!1385045122601). |
| if (srvInfo != null && srvInfo.contains("WebLogic")) { |
| sesIdTransformer = new C1<String, String>() { |
| @Override public String apply(String s) { |
| // Find first exclamation mark. |
| int idx = s.indexOf('!'); |
| |
| // Return original string if not found. |
| if (idx < 0 || idx == s.length() - 1) |
| return s; |
| |
| // Find second exclamation mark. |
| idx = s.indexOf('!', idx + 1); |
| |
| // Return original string if not found. |
| if (idx < 0) |
| return s; |
| |
| // Return the session ID without timestamp. |
| return s.substring(0, idx); |
| } |
| }; |
| } |
| |
| if (log.isInfoEnabled()) |
| log.info("Started web sessions caching [igniteInstanceName=" + igniteInstanceName + |
| ", cacheName=" + cacheName + ", maxRetriesOnFail=" + retries + ']'); |
| } |
| |
| /** |
| * Init cache. |
| */ |
| @SuppressWarnings("unchecked") |
| void initCache() { |
| cache = webSesIgnite.cache(cacheName); |
| binaryCache = webSesIgnite.cache(cacheName); |
| |
| if (cache == null) |
| throw new IgniteException("Cache for web sessions is not started (is it configured?): " + cacheName); |
| |
| CacheConfiguration cacheCfg = cache.getConfiguration(CacheConfiguration.class); |
| |
| if (cacheCfg.getWriteSynchronizationMode() == FULL_ASYNC) |
| throw new IgniteException("Cache for web sessions cannot be in FULL_ASYNC mode: " + cacheName); |
| |
| if (!cacheCfg.isEagerTtl()) |
| throw new IgniteException("Cache for web sessions cannot operate with lazy TTL. " + |
| "Consider setting eagerTtl to true for cache: " + cacheName); |
| |
| if (cacheCfg.getCacheMode() == LOCAL) |
| U.quietAndWarn(webSesIgnite.log(), "Using LOCAL cache for web sessions caching " + |
| "(this is only OK in test mode): " + cacheName); |
| |
| if (cacheCfg.getCacheMode() == PARTITIONED && cacheCfg.getAtomicityMode() != ATOMIC) |
| U.quietAndWarn(webSesIgnite.log(), "Using " + cacheCfg.getAtomicityMode() + " atomicity for web sessions " + |
| "caching (switch to ATOMIC mode for better performance)"); |
| |
| txEnabled = cacheCfg.getAtomicityMode() == TRANSACTIONAL; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public void destroy() { |
| // No-op. |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) |
| throws IOException, ServletException { |
| assert ctx != null; |
| |
| if (req instanceof HttpServletRequest) { |
| HttpServletRequest httpReq = (HttpServletRequest)req; |
| |
| String sesId = null; |
| |
| try { |
| if (txEnabled) { |
| try (Transaction tx = txs.txStart(PESSIMISTIC, REPEATABLE_READ)) { |
| sesId = doFilterDispatch(httpReq, res, chain); |
| |
| tx.commit(); |
| } |
| } |
| else |
| sesId = doFilterDispatch(httpReq, res, chain); |
| } |
| catch (Exception e) { |
| U.error(log, "Failed to update web session: " + sesId, e); |
| } |
| } |
| else |
| chain.doFilter(req, res); |
| } |
| |
| /** |
| * Use {@link WebSession} or {@link WebSessionV2} according to {@link #keepBinary} flag. |
| * |
| * @param httpReq Request. |
| * @param res Response. |
| * @param chain Filter chain. |
| * @return Session ID. |
| * @throws IOException |
| * @throws ServletException |
| * @throws CacheException |
| */ |
| private String doFilterDispatch(HttpServletRequest httpReq, ServletResponse res, FilterChain chain) |
| throws IOException, ServletException, CacheException { |
| if (keepBinary) |
| return doFilterV2(httpReq, res, chain); |
| |
| return doFilterV1(httpReq, res, chain); |
| } |
| |
| /** |
| * @param httpReq Request. |
| * @param res Response. |
| * @param chain Filter chain. |
| * @return Session ID. |
| * @throws IOException In case of I/O error. |
| * @throws ServletException In case of servlet error. |
| * @throws CacheException In case of other error. |
| */ |
| private String doFilterV1(HttpServletRequest httpReq, ServletResponse res, FilterChain chain) throws IOException, |
| ServletException, CacheException { |
| WebSession cached = null; |
| String sesId; |
| |
| if (httpReq.getSession(false) != null) |
| sesId = httpReq.getSession(false).getId(); |
| else |
| sesId = httpReq.getRequestedSessionId(); |
| |
| if (sesId != null) { |
| sesId = transformSessionId(sesId); |
| |
| for (int i = 0; i < retries; i++) { |
| try { |
| cached = cache.get(sesId); |
| |
| break; |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| handleLoadSessionException(sesId, i, e); |
| } |
| } |
| |
| if (cached != null) { |
| if (log.isDebugEnabled()) |
| log.debug("Using cached session for ID: " + sesId); |
| |
| if (cached.isNew()) |
| cached = new WebSession(cached.getId(), cached, false); |
| } |
| else { |
| if (log.isDebugEnabled()) |
| log.debug("Cached session was invalidated and doesn't exist: " + sesId); |
| |
| HttpSession ses = httpReq.getSession(false); |
| |
| if (ses != null) { |
| try { |
| ses.invalidate(); |
| } |
| catch (IllegalStateException ignore) { |
| // Session was already invalidated. |
| } |
| } |
| |
| cached = createSession(httpReq); |
| } |
| } |
| else |
| cached = createSession(httpReq); |
| |
| assert cached != null; |
| |
| sesId = cached.getId(); |
| |
| cached.servletContext(ctx); |
| cached.filter(this); |
| cached.resetUpdates(); |
| cached.genSes(httpReq.getSession(false)); |
| |
| httpReq = new RequestWrapper(httpReq, cached); |
| |
| chain.doFilter(httpReq, res); |
| |
| HttpSession ses = httpReq.getSession(false); |
| |
| if (ses != null && ses instanceof WebSession) { |
| Collection<T2<String, Object>> updates = ((WebSession)ses).updates(); |
| |
| if (updates != null) |
| updateAttributes(transformSessionId(sesId), updates, ses.getMaxInactiveInterval()); |
| } |
| |
| return sesId; |
| } |
| |
| /** |
| * @param httpReq Request. |
| * @param res Response. |
| * @param chain Filter chain. |
| * @return Session ID. |
| * @throws IOException In case of I/O error. |
| * @throws ServletException In case oif servlet error. |
| * @throws CacheException In case of other error. |
| */ |
| private String doFilterV2(HttpServletRequest httpReq, ServletResponse res, FilterChain chain) |
| throws IOException, ServletException, CacheException { |
| WebSessionV2 cached = null; |
| String sesId; |
| |
| if (httpReq.getSession(false) != null) |
| sesId = httpReq.getSession(false).getId(); |
| else |
| sesId = httpReq.getRequestedSessionId(); |
| |
| if (sesId != null) { |
| sesId = transformSessionId(sesId); |
| |
| // Load from cache. |
| for (int i = 0; i < retries; i++) { |
| try { |
| final WebSessionEntity entity = binaryCache.get(sesId); |
| |
| if (entity != null) |
| cached = new WebSessionV2(sesId, httpReq.getSession(false), false, ctx, entity, marshaller); |
| |
| break; |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| handleLoadSessionException(sesId, i, e); |
| } |
| } |
| |
| if (cached != null) { |
| if (log.isDebugEnabled()) |
| log.debug("Using cached session for ID: " + sesId); |
| } |
| // If not found - invalidate session and create new one. |
| // Invalidate, because session might be removed from cache |
| // according to expiry policy. |
| else { |
| if (log.isDebugEnabled()) |
| log.debug("Cached session was invalidated and doesn't exist: " + sesId); |
| |
| final HttpSession ses = httpReq.getSession(false); |
| |
| if (ses != null) { |
| try { |
| ses.invalidate(); |
| } |
| catch (IllegalStateException ignore) { |
| // Session was already invalidated. |
| } |
| } |
| |
| cached = createSessionV2(httpReq); |
| } |
| } |
| // No session was requested by the client, create new one and put in the request. |
| else |
| cached = createSessionV2(httpReq); |
| |
| assert cached != null; |
| |
| sesId = cached.getId(); |
| |
| httpReq = new RequestWrapperV2(httpReq, cached); |
| |
| chain.doFilter(httpReq, res); |
| |
| WebSessionV2 cachedNew = (WebSessionV2)httpReq.getSession(false); |
| |
| if (cachedNew != null && cachedNew.isValid()) |
| updateAttributesV2(cachedNew.getId(), cachedNew); |
| |
| return sesId; |
| } |
| |
| /** |
| * Log and process exception happened on loading session from cache. |
| * |
| * @param sesId Session ID. |
| * @param tryCnt Try count. |
| * @param e Caught exception. |
| */ |
| private void handleLoadSessionException(final String sesId, final int tryCnt, final RuntimeException e) { |
| if (log.isDebugEnabled()) |
| log.debug(e.getMessage()); |
| |
| if (tryCnt == retries - 1) |
| throw new IgniteException("Failed to handle request [session= " + sesId + "]", e); |
| else { |
| if (log.isDebugEnabled()) |
| log.debug("Failed to handle request (will retry): " + sesId); |
| |
| handleCacheOperationException(e); |
| } |
| } |
| |
| /** |
| * Transform session ID if ID transformer present. |
| * |
| * @param sesId Session ID to transform. |
| * @return Transformed session ID or the same if no session transformer available. |
| */ |
| private String transformSessionId(final String sesId) { |
| if (sesIdTransformer != null) |
| return sesIdTransformer.apply(sesId); |
| |
| return sesId; |
| } |
| |
| /** |
| * Creates a new session from http request. |
| * |
| * @param httpReq Request. |
| * @return New session. |
| */ |
| private WebSession createSession(HttpServletRequest httpReq) { |
| HttpSession ses = httpReq.getSession(true); |
| |
| String sesId = transformSessionId(ses.getId()); |
| |
| return createSession(ses, sesId); |
| } |
| |
| /** |
| * Creates a new web session with the specified id. |
| * |
| * @param ses Base session. |
| * @param sesId Session id. |
| * @return New session. |
| */ |
| private WebSession createSession(HttpSession ses, String sesId) { |
| WebSession cached = new WebSession(sesId, ses, true); |
| |
| cached.genSes(ses); |
| |
| if (log.isDebugEnabled()) |
| log.debug("Session created: " + sesId); |
| |
| for (int i = 0; i < retries; i++) { |
| try { |
| final IgniteCache<String, WebSession> cache0 = |
| cacheWithExpiryPolicy(cached.getMaxInactiveInterval(), cache); |
| |
| final WebSession old = cache0.getAndPutIfAbsent(sesId, cached); |
| |
| if (old != null) { |
| cached = old; |
| |
| if (cached.isNew()) |
| cached = new WebSession(cached.getId(), cached, false); |
| } |
| |
| break; |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| handleCreateSessionException(sesId, i, e); |
| } |
| } |
| |
| return cached; |
| } |
| |
| /** |
| * Log error and delegate exception processing to {@link #handleCacheOperationException(Exception)} |
| * |
| * @param sesId Session ID. |
| * @param tryCnt Try count. |
| * @param e Exception to process. |
| */ |
| private void handleCreateSessionException(final String sesId, final int tryCnt, final RuntimeException e) { |
| if (log.isDebugEnabled()) |
| log.debug(e.getMessage()); |
| |
| if (tryCnt == retries - 1) |
| throw new IgniteException("Failed to save session: " + sesId, e); |
| else { |
| if (log.isDebugEnabled()) |
| log.debug("Failed to save session (will retry): " + sesId); |
| |
| handleCacheOperationException(e); |
| } |
| } |
| |
| /** |
| * Creates a new web session with the specified id. |
| * |
| * @param ses Base session. |
| * @param sesId Session id. |
| * @return New session. |
| */ |
| private WebSessionV2 createSessionV2(final HttpSession ses, final String sesId) throws IOException { |
| assert ses != null; |
| assert sesId != null; |
| |
| WebSessionV2 cached = new WebSessionV2(sesId, ses, true, ctx, null, marshaller); |
| |
| final WebSessionEntity marshaledEntity = cached.marshalAttributes(); |
| |
| for (int i = 0; i < retries; i++) { |
| try { |
| final IgniteCache<String, WebSessionEntity> cache0 = cacheWithExpiryPolicy( |
| cached.getMaxInactiveInterval(), binaryCache); |
| |
| final WebSessionEntity old = cache0.getAndPutIfAbsent(sesId, marshaledEntity); |
| |
| if (old != null) |
| cached = new WebSessionV2(sesId, ses, false, ctx, old, marshaller); |
| else |
| cached = new WebSessionV2(sesId, ses, false, ctx, marshaledEntity, marshaller); |
| |
| break; |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| handleCreateSessionException(sesId, i, e); |
| } |
| } |
| |
| return cached; |
| } |
| |
| /** |
| * @param httpReq HTTP request. |
| * @return Cached session. |
| */ |
| private WebSessionV2 createSessionV2(HttpServletRequest httpReq) throws IOException { |
| final HttpSession ses = httpReq.getSession(true); |
| |
| final String sesId = transformSessionId(ses.getId()); |
| |
| if (log.isDebugEnabled()) |
| log.debug("Session created: " + sesId); |
| |
| return createSessionV2(ses, sesId); |
| } |
| |
| /** |
| * @param maxInactiveInteval Interval to use in expiry policy. |
| * @param cache Cache. |
| * @param <T> Cached object type. |
| * @return Cache with expiry policy if {@code maxInactiveInteval} greater than zero. |
| */ |
| private <T> IgniteCache<String, T> cacheWithExpiryPolicy(final int maxInactiveInteval, |
| final IgniteCache<String, T> cache) { |
| if (maxInactiveInteval > 0) { |
| long ttl = maxInactiveInteval * 1000L; |
| |
| ExpiryPolicy plc = new ModifiedExpiryPolicy(new Duration(MILLISECONDS, ttl)); |
| |
| return cache.withExpiryPolicy(plc); |
| } |
| |
| return cache; |
| } |
| |
| /** |
| * @param sesId Session ID. |
| */ |
| public void destroySession(String sesId) { |
| assert sesId != null; |
| |
| for (int i = 0; i < retries; i++) { |
| try { |
| if (cache.remove(sesId) && log.isDebugEnabled()) |
| log.debug("Session destroyed: " + sesId); |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| if (i == retries - 1) { |
| U.warn(log, "Failed to remove session [sesId=" + |
| sesId + ", retries=" + retries + ']'); |
| } |
| else { |
| U.warn(log, "Failed to remove session (will retry): " + sesId); |
| |
| handleCacheOperationException(e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param sesId Session ID. |
| * @param updates Updates list. |
| * @param maxInactiveInterval Max session inactive interval. |
| */ |
| public void updateAttributes(String sesId, Collection<T2<String, Object>> updates, int maxInactiveInterval) { |
| assert sesId != null; |
| assert updates != null; |
| |
| if (log.isDebugEnabled()) |
| log.debug("Session attributes updated [id=" + sesId + ", updates=" + updates + ']'); |
| |
| try { |
| for (int i = 0; i < retries; i++) { |
| try { |
| final IgniteCache<String, WebSession> cache0 = cacheWithExpiryPolicy(maxInactiveInterval, cache); |
| |
| cache0.invoke(sesId, WebSessionListener.newAttributeProcessor(updates)); |
| |
| break; |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| handleAttributeUpdateException(sesId, i, e); |
| } |
| } |
| } |
| catch (Exception e) { |
| U.error(log, "Failed to update session attributes [id=" + sesId + ']', e); |
| } |
| } |
| |
| /** |
| * @param sesId Session ID. |
| * @param ses Web session. |
| */ |
| public void updateAttributesV2(final String sesId, final WebSessionV2 ses) throws IOException { |
| assert sesId != null; |
| assert ses != null; |
| |
| final Map<String, byte[]> updatesMap = ses.binaryUpdatesMap(); |
| |
| if (log.isDebugEnabled()) |
| log.debug("Session binary attributes updated [id=" + sesId + ", updates=" + updatesMap.keySet() + ']'); |
| |
| try { |
| for (int i = 0; i < retries; i++) { |
| try { |
| final IgniteCache<String, WebSessionEntity> cache0 = |
| cacheWithExpiryPolicy(ses.getMaxInactiveInterval(), binaryCache); |
| |
| cache0.invoke(sesId, new WebSessionAttributeProcessor(updatesMap.isEmpty() ? null : updatesMap, |
| ses.getLastAccessedTime(), ses.getMaxInactiveInterval(), ses.isMaxInactiveIntervalChanged())); |
| |
| break; |
| } |
| catch (CacheException | IgniteException | IllegalStateException e) { |
| handleAttributeUpdateException(sesId, i, e); |
| } |
| } |
| } |
| catch (Exception e) { |
| U.error(log, "Failed to update session V2 attributes [id=" + sesId + ']', e); |
| } |
| } |
| |
| /** |
| * Log error and delegate processing to {@link #handleCacheOperationException(Exception)}. |
| * |
| * @param sesId Session ID. |
| * @param tryCnt Try count. |
| * @param e Exception to process. |
| */ |
| private void handleAttributeUpdateException(final String sesId, final int tryCnt, final RuntimeException e) { |
| if (tryCnt == retries - 1) { |
| U.error(log, "Failed to apply updates for session (maximum number of retries exceeded) [sesId=" + |
| sesId + ", retries=" + retries + ']', e); |
| } |
| else { |
| U.warn(log, "Failed to apply updates for session (will retry): " + sesId); |
| |
| handleCacheOperationException(e); |
| } |
| } |
| |
| |
| /** |
| * Handles cache operation exception. |
| * @param e Exception |
| */ |
| void handleCacheOperationException(Exception e) { |
| IgniteFuture<?> retryFut = null; |
| |
| if (e instanceof IllegalStateException) { |
| initCache(); |
| |
| return; |
| } |
| else if (X.hasCause(e, IgniteClientDisconnectedException.class)) { |
| IgniteClientDisconnectedException cause = X.cause(e, IgniteClientDisconnectedException.class); |
| |
| assert cause != null : e; |
| |
| retryFut = cause.reconnectFuture(); |
| } |
| else if (X.hasCause(e, ClusterTopologyException.class)) { |
| ClusterTopologyException cause = X.cause(e, ClusterTopologyException.class); |
| |
| assert cause != null : e; |
| |
| retryFut = cause.retryReadyFuture(); |
| } |
| |
| if (retryFut != null) { |
| try { |
| retryFut.get(retriesTimeout); |
| } |
| catch (IgniteException retryErr) { |
| throw new IgniteException("Failed to wait for retry: " + retryErr); |
| } |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public String toString() { |
| return S.toString(WebSessionFilter.class, this); |
| } |
| |
| /** |
| * Request wrapper. |
| */ |
| private class RequestWrapper extends HttpServletRequestWrapper { |
| /** Session. */ |
| private volatile WebSession ses; |
| |
| /** |
| * @param req Request. |
| * @param ses Session. |
| */ |
| private RequestWrapper(HttpServletRequest req, WebSession ses) { |
| super(req); |
| |
| assert ses != null; |
| |
| this.ses = ses; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public HttpSession getSession(boolean create) { |
| if (!ses.isValid()) { |
| if (create) { |
| this.ses = createSession((HttpServletRequest)getRequest()); |
| this.ses.servletContext(ctx); |
| this.ses.filter(WebSessionFilter.this); |
| this.ses.resetUpdates(); |
| } |
| else |
| return null; |
| } |
| |
| return ses; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public HttpSession getSession() { |
| return getSession(true); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public String changeSessionId() { |
| HttpServletRequest req = (HttpServletRequest)getRequest(); |
| |
| String newId = req.changeSessionId(); |
| |
| this.ses.setId(newId); |
| |
| this.ses = createSession(ses, newId); |
| this.ses.servletContext(ctx); |
| this.ses.filter(WebSessionFilter.this); |
| this.ses.resetUpdates(); |
| |
| return newId; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public void login(String username, String password) throws ServletException { |
| HttpServletRequest req = (HttpServletRequest)getRequest(); |
| |
| req.login(username, password); |
| |
| String newId = req.getSession(false).getId(); |
| |
| this.ses.setId(newId); |
| |
| this.ses = createSession(ses, newId); |
| this.ses.servletContext(ctx); |
| this.ses.filter(WebSessionFilter.this); |
| this.ses.resetUpdates(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public boolean isRequestedSessionIdValid() { |
| return ses.isValid(); |
| } |
| } |
| |
| /** |
| * Request wrapper V2. |
| */ |
| private class RequestWrapperV2 extends HttpServletRequestWrapper { |
| /** Session. */ |
| private WebSessionV2 ses; |
| |
| /** |
| * @param req Request. |
| * @param ses Session. |
| */ |
| private RequestWrapperV2(HttpServletRequest req, WebSessionV2 ses) { |
| super(req); |
| |
| assert ses != null; |
| |
| this.ses = ses; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public HttpSession getSession(boolean create) { |
| if (ses != null && !ses.isValid()) { |
| binaryCache.remove(ses.id()); |
| |
| if (create) { |
| try { |
| ses = createSessionV2((HttpServletRequest)getRequest()); |
| } |
| catch (IOException e) { |
| throw new IgniteException(e); |
| } |
| } |
| else |
| ses = null; |
| } |
| |
| return ses; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public HttpSession getSession() { |
| return getSession(true); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public String changeSessionId() { |
| final HttpServletRequest req = (HttpServletRequest)getRequest(); |
| |
| final String newId = req.changeSessionId(); |
| |
| if (!F.eq(newId, ses.getId())) { |
| try { |
| ses = createSessionV2(ses, newId); |
| } |
| catch (IOException e) { |
| throw new IgniteException(e); |
| } |
| } |
| |
| return newId; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public void login(String username, String password) throws ServletException { |
| final HttpServletRequest req = (HttpServletRequest)getRequest(); |
| |
| req.login(username, password); |
| |
| final String newId = req.getSession(false).getId(); |
| |
| if (!F.eq(newId, ses.getId())) { |
| try { |
| ses = createSessionV2(ses, newId); |
| } |
| catch (IOException e) { |
| throw new IgniteException(e); |
| } |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public boolean isRequestedSessionIdValid() { |
| return ses != null && ses.isValid(); |
| } |
| } |
| } |