IGNITE-3087: WebSessions: User no longer required to have session classes and servlet/ignite-web in the classpath of server node.
Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/ee1cd486 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/ee1cd486 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/ee1cd486 Branch: refs/heads/ignite-3163 Commit: ee1cd486fdb91603d7a69a1cf0166c5ed13dc383 Parents: 8731e46 Author: dkarachentsev <[email protected]> Authored: Thu May 12 11:44:20 2016 +0300 Committer: vozerov-gridgain <[email protected]> Committed: Thu May 12 11:44:20 2016 +0300 ---------------------------------------------------------------------- .../WebSessionAttributeProcessor.java | 135 ++++++ .../internal/websession/WebSessionEntity.java | 193 ++++++++ .../ignite/cache/websession/WebSession.java | 13 +- .../cache/websession/WebSessionFilter.java | 447 ++++++++++++++++--- .../cache/websession/WebSessionListener.java | 117 +---- .../ignite/cache/websession/WebSessionV2.java | 397 ++++++++++++++++ .../internal/websession/WebSessionSelfTest.java | 133 +++++- .../webapp2/META-INF/ignite-webapp-config.xml | 288 ++++++++++++ modules/web/src/test/webapp2/WEB-INF/web.xml | 37 ++ 9 files changed, 1575 insertions(+), 185 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionAttributeProcessor.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionAttributeProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionAttributeProcessor.java new file mode 100644 index 0000000..35b4d90 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionAttributeProcessor.java @@ -0,0 +1,135 @@ +/* + * 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.internal.websession; + +import org.apache.ignite.binary.BinaryObjectException; +import org.apache.ignite.binary.BinaryRawReader; +import org.apache.ignite.binary.BinaryRawWriter; +import org.apache.ignite.binary.BinaryReader; +import org.apache.ignite.binary.BinaryWriter; +import org.apache.ignite.binary.Binarylizable; +import org.apache.ignite.internal.util.typedef.F; + +import javax.cache.processor.EntryProcessor; +import javax.cache.processor.EntryProcessorException; +import javax.cache.processor.MutableEntry; +import java.io.Serializable; +import java.util.Map; + +/** + * Updates web session attributes according to {@link #updatesMap} and {@link #accessTime}, + * {@link #maxInactiveInterval}. + */ +public class WebSessionAttributeProcessor implements EntryProcessor<String, WebSessionEntity, Void>, + Serializable, Binarylizable { + /** */ + private static final long serialVersionUID = 0L; + + /** Attribute updates. */ + private Map<String, byte[]> updatesMap; + + /** Access time update. */ + private long accessTime; + + /** Max inactive interval update. */ + private int maxInactiveInterval; + + /** Indicates whether apply or not max inactive interval update. */ + private boolean maxIntervalChanged; + + /** + * Empty constructor for serialization. + */ + public WebSessionAttributeProcessor() { + // No-op. + } + + /** + * Constructs attribute processor. + * + * @param updatesMap Updates that should be performed on entity attributes. + * @param accessTime Access time. + * @param maxInactiveInterval Max inactive interval. + * @param maxIntervalChanged {@code True} if max inactive interval should be updated. + */ + public WebSessionAttributeProcessor( + final Map<String, byte[]> updatesMap, final long accessTime, final int maxInactiveInterval, + final boolean maxIntervalChanged) { + this.updatesMap = updatesMap; + this.accessTime = accessTime; + this.maxInactiveInterval = maxInactiveInterval; + this.maxIntervalChanged = maxIntervalChanged; + } + + /** {@inheritDoc} */ + @Override public void writeBinary(final BinaryWriter writer) throws BinaryObjectException { + final BinaryRawWriter rawWriter = writer.rawWriter(); + + rawWriter.writeMap(updatesMap); + rawWriter.writeLong(accessTime); + rawWriter.writeBoolean(maxIntervalChanged); + + if (maxIntervalChanged) + rawWriter.writeInt(maxInactiveInterval); + } + + /** {@inheritDoc} */ + @Override public void readBinary(final BinaryReader reader) throws BinaryObjectException { + final BinaryRawReader rawReader = reader.rawReader(); + + updatesMap = rawReader.readMap(); + accessTime = rawReader.readLong(); + maxIntervalChanged = rawReader.readBoolean(); + + if (maxIntervalChanged) + maxInactiveInterval = rawReader.readInt(); + } + + /** {@inheritDoc} */ + @Override public Void process(final MutableEntry<String, WebSessionEntity> entry, + final Object... arguments) throws EntryProcessorException { + final WebSessionEntity entity = entry.getValue(); + + final WebSessionEntity newEntity = new WebSessionEntity(entity); + + if (newEntity.accessTime() < accessTime) + newEntity.accessTime(accessTime); + + if (maxIntervalChanged) + newEntity.maxInactiveInterval(maxInactiveInterval); + + if (!F.isEmpty(updatesMap)) { + for (final Map.Entry<String, byte[]> update : updatesMap.entrySet()) { + final String name = update.getKey(); + + assert name != null; + + final byte[] val = update.getValue(); + + if (val != null) + newEntity.putAttribute(name, val); + else + newEntity.removeAttribute(name); + } + } + + entry.setValue(newEntity); + + return null; + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionEntity.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionEntity.java b/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionEntity.java new file mode 100644 index 0000000..c504ee0 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/websession/WebSessionEntity.java @@ -0,0 +1,193 @@ +/* + * 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.internal.websession; + +import org.apache.ignite.binary.BinaryObjectException; +import org.apache.ignite.binary.BinaryRawReader; +import org.apache.ignite.binary.BinaryRawWriter; +import org.apache.ignite.binary.BinaryReader; +import org.apache.ignite.binary.BinaryWriter; +import org.apache.ignite.binary.Binarylizable; +import org.apache.ignite.internal.util.tostring.GridToStringExclude; +import org.apache.ignite.internal.util.typedef.internal.S; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Entity + */ +public class WebSessionEntity implements Serializable, Binarylizable { + /** */ + private static final long serialVersionUID = 0L; + + /** Session ID. */ + private String id; + + /** Creation time. */ + private long createTime; + + /** Last access time. */ + private long accessTime; + + /** Maximum inactive interval. */ + private int maxInactiveInterval; + + /** Attributes. */ + @GridToStringExclude + private Map<String, byte[]> attrs; + + /** + * Constructor. + */ + public WebSessionEntity() { + // No-op. + } + + /** + * Constructor. + * + * @param id Session ID. + * @param createTime Session create time. + * @param accessTime Session last access time. + * @param maxInactiveInterval Session will be removed if not accessed more then this value. + */ + public WebSessionEntity(final String id, final long createTime, final long accessTime, + final int maxInactiveInterval) { + this.id = id; + this.createTime = createTime; + this.accessTime = accessTime; + this.maxInactiveInterval = maxInactiveInterval; + } + + /** + * Constructor. + */ + public WebSessionEntity(final WebSessionEntity other) { + this(other.id(), other.createTime(), other.accessTime(), other.maxInactiveInterval()); + + if (!other.attributes().isEmpty()) + attrs = new HashMap<>(other.attributes()); + } + + /** + * @return Session ID. + */ + public String id() { + return id; + } + + /** + * @return Create time. + */ + public long createTime() { + return createTime; + } + + /** + * @return Access time. + */ + public long accessTime() { + return accessTime; + } + + /** + * Set access time. + * + * @param accessTime Access time. + */ + public void accessTime(final long accessTime) { + this.accessTime = accessTime; + } + + /** + * @return Max inactive interval. + */ + public int maxInactiveInterval() { + return maxInactiveInterval; + } + + /** + * Set max inactive interval; + * + * @param maxInactiveInterval Max inactive interval. + */ + public void maxInactiveInterval(final int maxInactiveInterval) { + this.maxInactiveInterval = maxInactiveInterval; + } + + /** + * @return Session attributes or {@link Collections#emptyMap()}. + */ + public Map<String, byte[]> attributes() { + return attrs == null ? Collections.<String, byte[]>emptyMap() : attrs; + } + + /** + * Add attribute to attribute map. + * + * @param name Attribute name. + * @param val Attribute value. + */ + public void putAttribute(final String name, final byte[] val) { + if (attrs == null) + attrs = new HashMap<>(); + + attrs.put(name, val); + } + + /** + * Remove attribute. + * + * @param name Attribute name. + */ + public void removeAttribute(final String name) { + if (attrs != null) + attrs.remove(name); + } + + /** {@inheritDoc} */ + @Override public void writeBinary(final BinaryWriter writer) throws BinaryObjectException { + final BinaryRawWriter rawWriter = writer.rawWriter(); + + rawWriter.writeString(id); + rawWriter.writeLong(createTime); + rawWriter.writeLong(accessTime); + rawWriter.writeInt(maxInactiveInterval); + rawWriter.writeMap(attrs); + } + + /** {@inheritDoc} */ + @Override public void readBinary(final BinaryReader reader) throws BinaryObjectException { + final BinaryRawReader rawReader = reader.rawReader(); + + id = rawReader.readString(); + createTime = rawReader.readLong(); + accessTime = rawReader.readLong(); + maxInactiveInterval = rawReader.readInt(); + attrs = rawReader.readMap(); + } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(WebSessionEntity.class, this, + "attributes", attrs != null ? attrs.keySet() : Collections.<String>emptySet()); + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSession.java ---------------------------------------------------------------------- diff --git a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSession.java b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSession.java index 05ecc36..5734b64 100644 --- a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSession.java +++ b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSession.java @@ -76,9 +76,8 @@ class WebSession implements HttpSession, Externalizable { @GridToStringExclude private transient ServletContext ctx; - /** Listener. */ @GridToStringExclude - private transient WebSessionListener lsnr; + private transient WebSessionFilter filter; /** New session flag. */ private transient boolean isNew; @@ -140,12 +139,12 @@ class WebSession implements HttpSession, Externalizable { } /** - * @param lsnr Listener. + * @param filter Filter. */ - public void listener(WebSessionListener lsnr) { - assert lsnr != null; + public void filter(final WebSessionFilter filter) { + assert filter != null; - this.lsnr = lsnr; + this.filter = filter; } /** @@ -248,7 +247,7 @@ class WebSession implements HttpSession, Externalizable { updates = null; - lsnr.destroySession(id); + filter.destroySession(id); } /** {@inheritDoc} */ http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionFilter.java ---------------------------------------------------------------------- diff --git a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionFilter.java b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionFilter.java index 02b5f65..c0f62bf 100644 --- a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionFilter.java +++ b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionFilter.java @@ -19,6 +19,7 @@ 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; @@ -42,13 +43,17 @@ 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; @@ -182,21 +187,28 @@ public class WebSessionFilter implements Filter { /** 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; + // TOOD: Minimal JavaDoc. + /** */ + 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; - /** Listener. */ - private WebSessionListener lsnr; - /** Logger. */ private IgniteLogger log; @@ -221,6 +233,12 @@ public class WebSessionFilter implements Filter { /** */ 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(); @@ -256,6 +274,11 @@ public class WebSessionFilter implements Filter { 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(gridName); if (webSesIgnite == null) @@ -266,9 +289,9 @@ public class WebSessionFilter implements Filter { log = webSesIgnite.log(); - initCache(); + marshaller = webSesIgnite.configuration().getMarshaller(); - lsnr = new WebSessionListener(webSesIgnite, this, retries); + initCache(); String srvInfo = ctx.getServerInfo(); @@ -304,17 +327,12 @@ public class WebSessionFilter implements Filter { } /** - * @return Cache. - */ - IgniteCache<String, WebSession> getCache(){ - return cache; - } - - /** * 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); @@ -357,13 +375,13 @@ public class WebSessionFilter implements Filter { try { if (txEnabled) { try (Transaction tx = txs.txStart(PESSIMISTIC, REPEATABLE_READ)) { - sesId = doFilter0(httpReq, res, chain); + sesId = doFilterDispatch(httpReq, res, chain); tx.commit(); } } else - sesId = doFilter0(httpReq, res, chain); + sesId = doFilterDispatch(httpReq, res, chain); } catch (Exception e) { U.error(log, "Failed to update web session: " + sesId, e); @@ -374,6 +392,25 @@ public class WebSessionFilter implements Filter { } /** + * 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. @@ -382,32 +419,23 @@ public class WebSessionFilter implements Filter { * @throws ServletException In case oif servlet error. * @throws CacheException In case of other error. */ - private String doFilter0(HttpServletRequest httpReq, ServletResponse res, FilterChain chain) throws IOException, + private String doFilterV1(HttpServletRequest httpReq, ServletResponse res, FilterChain chain) throws IOException, ServletException, CacheException { WebSession cached = null; String sesId = httpReq.getRequestedSessionId(); if (sesId != null) { - if (sesIdTransformer != null) - sesId = sesIdTransformer.apply(sesId); + sesId = transformSessionId(sesId); for (int i = 0; i < retries; i++) { try { cached = cache.get(sesId); + + break; } catch (CacheException | IgniteException | IllegalStateException e) { - if (log.isDebugEnabled()) - log.debug(e.getMessage()); - - if (i == 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); - } + handleLoadSessionException(sesId, i, e); } } @@ -445,23 +473,132 @@ public class WebSessionFilter implements Filter { assert cached != null; cached.servletContext(ctx); - cached.listener(lsnr); + cached.filter(this); cached.resetUpdates(); httpReq = new RequestWrapper(httpReq, cached); chain.doFilter(httpReq, res); - HttpSession ses = httpReq.getSession(false); + final Collection<T2<String, Object>> updates = cached.updates(); + + if (updates != null) + updateAttributes(transformSessionId(sesId), updates, cached.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 = 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, null, 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 && ses instanceof WebSession) { - Collection<T2<String, Object>> updates = ((WebSession)ses).updates(); + if (ses != null) { + try { + ses.invalidate(); + } + catch (IllegalStateException ignore) { + // Session was already invalidated. + } + } - if (updates != null) { - lsnr.updateAttributes(sesIdTransformer != null ? sesIdTransformer.apply(ses.getId()) : ses.getId(), - updates, ses.getMaxInactiveInterval()); + cached = createSessionV2(httpReq); } } + // No session was requested by the client, create new one and put in the request. + else { + cached = createSessionV2(httpReq); + + sesId = cached.getId(); + } + + assert cached != null; + + httpReq = new RequestWrapperV2(httpReq, cached); + + chain.doFilter(httpReq, res); + + // Update session + if (cached.isValid()) + updateAttributesV2(sesId, cached); + else + binaryCache.remove(sesId); + + 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; } @@ -474,7 +611,7 @@ public class WebSessionFilter implements Filter { private WebSession createSession(HttpServletRequest httpReq) { HttpSession ses = httpReq.getSession(true); - String sesId = sesIdTransformer != null ? sesIdTransformer.apply(ses.getId()) : ses.getId(); + String sesId = transformSessionId(ses.getId()); if (log.isDebugEnabled()) log.debug("Session created: " + sesId); @@ -483,19 +620,10 @@ public class WebSessionFilter implements Filter { for (int i = 0; i < retries; i++) { try { - IgniteCache<String, WebSession> cache0; - - if (cached.getMaxInactiveInterval() > 0) { - long ttl = cached.getMaxInactiveInterval() * 1000; + final IgniteCache<String, WebSession> cache0 = + cacheWithExpiryPolicy(cached.getMaxInactiveInterval(), cache); - ExpiryPolicy plc = new ModifiedExpiryPolicy(new Duration(MILLISECONDS, ttl)); - - cache0 = cache.withExpiryPolicy(plc); - } - else - cache0 = cache; - - WebSession old = cache0.getAndPutIfAbsent(sesId, cached); + final WebSession old = cache0.getAndPutIfAbsent(sesId, cached); if (old != null) { cached = old; @@ -507,27 +635,206 @@ public class WebSessionFilter implements Filter { break; } catch (CacheException | IgniteException | IllegalStateException e) { - if (log.isDebugEnabled()) - log.debug(e.getMessage()); + handleCreateSessionException(sesId, i, e); + } + } - if (i == retries - 1) - throw new IgniteException("Failed to save session: " + sesId, 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); + } + } + + /** + * @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); + + 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, null, false, ctx, old, marshaller); + else + cached = new WebSessionV2(sesId, null, false, ctx, marshaledEntity, marshaller); + + break; + } + catch (CacheException | IgniteException | IllegalStateException e) { + handleCreateSessionException(sesId, i, e); + } + } + + return cached; + } + + /** + * @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 * 1000; + + 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 { - if (log.isDebugEnabled()) - log.debug("Failed to save session (will retry): " + sesId); + U.warn(log, "Failed to remove session (will retry): " + sesId); handleCacheOperationException(e); } } } + } - return cached; + /** + * @param sesId Session ID. + * @param updates Updates list. + * @param maxInactiveInterval Max session inactive interval. + */ + @SuppressWarnings("unchecked") + 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.warn(log, "Failed to apply updates for session (maximum number of retries exceeded) [sesId=" + + sesId + ", retries=" + retries + ']'); + } + else { + U.warn(log, "Failed to apply updates for session (will retry): " + sesId); + + handleCacheOperationException(e); + } + } + + + /** * Handles cache operation exception. * @param e Exception */ + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") void handleCacheOperationException(Exception e){ IgniteFuture<?> retryFut = null; @@ -595,4 +902,34 @@ public class WebSessionFilter implements Filter { return ses; } } + + /** + * Request wrapper V2. + */ + private static class RequestWrapperV2 extends HttpServletRequestWrapper { + /** Session. */ + private final 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) { + return ses; + } + + /** {@inheritDoc} */ + @Override public HttpSession getSession() { + return ses; + } + } } http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionListener.java ---------------------------------------------------------------------- diff --git a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionListener.java b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionListener.java index 0d8ffec..ab41879 100644 --- a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionListener.java +++ b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionListener.java @@ -22,127 +22,24 @@ import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.Collection; -import javax.cache.CacheException; -import javax.cache.expiry.Duration; -import javax.cache.expiry.ExpiryPolicy; -import javax.cache.expiry.ModifiedExpiryPolicy; import javax.cache.processor.EntryProcessor; import javax.cache.processor.MutableEntry; -import org.apache.ignite.Ignite; -import org.apache.ignite.IgniteCache; -import org.apache.ignite.IgniteException; -import org.apache.ignite.IgniteLogger; import org.apache.ignite.internal.util.typedef.T2; -import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.internal.util.typedef.internal.U; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - /** * Session listener for web sessions caching. */ class WebSessionListener { - /** Filter. */ - private final WebSessionFilter filter; - - /** Maximum retries. */ - private final int retries; - - /** Logger. */ - private final IgniteLogger log; - - /** - * @param ignite Grid. - * @param filter Filter. - * @param retries Maximum retries. - */ - WebSessionListener(Ignite ignite, WebSessionFilter filter, int retries) { - assert ignite != null; - assert filter != null; - - this.filter = filter; - this.retries = retries > 0 ? retries : 1; - - log = ignite.log(); - } - - /** - * @param sesId Session ID. - */ - public void destroySession(String sesId) { - assert sesId != null; - for (int i = 0; i < retries; i++) { - try { - if (filter.getCache().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); - - filter.handleCacheOperationException(e); - } - } - } - } - /** - * @param sesId Session ID. - * @param updates Updates list. - * @param maxInactiveInterval Max session inactive interval. + * Creates new instance of attribute processor. Used for compatibility. + * + * @param updates Updates. + * @return New instance of attribute processor. */ - @SuppressWarnings("unchecked") - 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 { - IgniteCache<String, WebSession> cache0; - - if (maxInactiveInterval > 0) { - long ttl = maxInactiveInterval * 1000; - - ExpiryPolicy plc = new ModifiedExpiryPolicy(new Duration(MILLISECONDS, ttl)); - - cache0 = filter.getCache().withExpiryPolicy(plc); - } - else - cache0 = filter.getCache(); - - cache0.invoke(sesId, new AttributesProcessor(updates)); - - break; - } - catch (CacheException | IgniteException | IllegalStateException e) { - if (i == retries - 1) { - U.warn(log, "Failed to apply updates for session (maximum number of retries exceeded) [sesId=" + - sesId + ", retries=" + retries + ']'); - } - else { - U.warn(log, "Failed to apply updates for session (will retry): " + sesId); - - filter.handleCacheOperationException(e); - } - } - } - } - catch (Exception e) { - U.error(log, "Failed to update session attributes [id=" + sesId + ']', e); - } - } - - /** {@inheritDoc} */ - @Override public String toString() { - return S.toString(WebSessionListener.class, this); + public static EntryProcessor<String, WebSession, Void> newAttributeProcessor( + final Collection<T2<String, Object>> updates) { + return new AttributesProcessor(updates); } /** http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionV2.java ---------------------------------------------------------------------- diff --git a/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionV2.java b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionV2.java new file mode 100644 index 0000000..53f97a2 --- /dev/null +++ b/modules/web/src/main/java/org/apache/ignite/cache/websession/WebSessionV2.java @@ -0,0 +1,397 @@ +/* + * 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 org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteException; +import org.apache.ignite.internal.util.tostring.GridToStringExclude; +import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.websession.WebSessionEntity; +import org.apache.ignite.marshaller.Marshaller; +import org.jetbrains.annotations.Nullable; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionContext; +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Session implementation that uses internal entity, which stores binary attributes, + * for safe caching and processing on remote nodes. + */ +class WebSessionV2 implements HttpSession { + /** Empty session context. */ + @SuppressWarnings("deprecation") + private static final HttpSessionContext EMPTY_SES_CTX = new HttpSessionContext() { + @Nullable @Override public HttpSession getSession(String id) { + return null; + } + + @Override public Enumeration<String> getIds() { + return Collections.enumeration(Collections.<String>emptyList()); + } + }; + + /** Placeholder for removed attribute. */ + private static final Object REMOVED_ATTR = new Object(); + + /** Servlet context. */ + @GridToStringExclude + private final ServletContext ctx; + + /** Entity that holds binary attributes. */ + private WebSessionEntity entity; + + /** Attributes. */ + protected Map<String, Object> attrs; + + /** Attributes waiting for update in cache. */ + private Map<String, Object> updatesMap; + + /** Timestamp that shows when this object was created. (Last access time from user request) */ + private final long accessTime; + + /** Cached session TTL since last query. */ + private int maxInactiveInterval; + + /** Flag indicates if {@link #maxInactiveInterval} waiting for update in cache. */ + private boolean maxInactiveIntervalChanged; + + /** New session flag. */ + private boolean isNew; + + /** Session invalidation flag. */ + private boolean invalidated; + + /** Grid marshaller. */ + private final Marshaller marshaller; + + /** + * @param id Session ID. + * @param ses Session. + * @param isNew Is new flag. + */ + WebSessionV2(final String id, final @Nullable HttpSession ses, final boolean isNew, final ServletContext ctx, + @Nullable WebSessionEntity entity, final Marshaller marshaller) { + assert id != null; + assert marshaller != null; + assert ctx != null; + assert ses != null || entity != null; + + this.marshaller = marshaller; + this.ctx = ctx; + this.isNew = isNew; + + accessTime = System.currentTimeMillis(); + + if (entity == null) { + entity = new WebSessionEntity(id, ses.getCreationTime(), accessTime, + ses.getMaxInactiveInterval()); + } + + this.entity = entity; + + maxInactiveInterval = entity.maxInactiveInterval(); + + if (ses != null) { + final Enumeration<String> names = ses.getAttributeNames(); + + while (names.hasMoreElements()) { + final String name = names.nextElement(); + + attributes().put(name, ses.getAttribute(name)); + } + } + } + + /** {@inheritDoc} */ + @Override public long getCreationTime() { + assertValid(); + + return entity.createTime(); + } + + /** {@inheritDoc} */ + @Override public String getId() { + assertValid(); + + return entity.id(); + } + + /** {@inheritDoc} */ + @Override public long getLastAccessedTime() { + assertValid(); + + return accessTime; + } + + /** {@inheritDoc} */ + @Override public ServletContext getServletContext() { + return ctx; + } + + /** {@inheritDoc} */ + @Override public void setMaxInactiveInterval(final int interval) { + maxInactiveInterval = interval; + + maxInactiveIntervalChanged = true; + } + + /** + * @return {@code True} if {@link #setMaxInactiveInterval(int)} was invoked. + */ + public boolean isMaxInactiveIntervalChanged() { + return maxInactiveIntervalChanged; + } + + /** {@inheritDoc} */ + @Override public int getMaxInactiveInterval() { + return maxInactiveInterval; + } + + /** {@inheritDoc} */ + @SuppressWarnings("deprecation") + @Override public HttpSessionContext getSessionContext() { + return EMPTY_SES_CTX; + } + + /** {@inheritDoc} */ + @Override public Object getAttribute(final String name) { + assertValid(); + + Object attr = attributes().get(name); + + if (attr == REMOVED_ATTR) + return null; + + if (attr != null) + return attr; + + final byte[] bytes = entity.attributes().get(name); + + if (bytes != null) { + // deserialize + try { + attr = unmarshal(bytes); + } + catch (IOException e) { + throw new IgniteException(e); + } + + attributes().put(name, attr); + } + + return attr; + } + + /** {@inheritDoc} */ + @Override public Object getValue(final String name) { + return getAttribute(name); + } + + /** {@inheritDoc} */ + @Override public void setAttribute(final String name, final Object val) { + assertValid(); + + attributes().put(name, val); + + updatesMap().put(name, val); + } + + /** {@inheritDoc} */ + @Override public void putValue(final String name, final Object val) { + setAttribute(name, val); + } + + /** {@inheritDoc} */ + @Override public Enumeration<String> getAttributeNames() { + assertValid(); + + return Collections.enumeration(attributeNames()); + } + + /** + * @return Set of attribute names. + */ + private Set<String> attributeNames() { + if (!F.isEmpty(attrs)) { + final Set<String> names = new HashSet<>(entity.attributes().size() + attrs.size()); + + names.addAll(entity.attributes().keySet()); + + for (final Map.Entry<String, Object> entry : attrs.entrySet()) { + if (entry != REMOVED_ATTR) + names.add(entry.getKey()); + } + + return names; + } + + return entity.attributes().keySet(); + } + + /** {@inheritDoc} */ + @Override public String[] getValueNames() { + assertValid(); + + final Set<String> names = attributeNames(); + + return names.toArray(new String[names.size()]); + } + + /** {@inheritDoc} */ + @Override public void removeAttribute(final String name) { + assertValid(); + + attributes().put(name, REMOVED_ATTR); + + updatesMap().put(name, null); + } + + /** {@inheritDoc} */ + @Override public void removeValue(final String name) { + removeAttribute(name); + } + + /** {@inheritDoc} */ + @Override public void invalidate() { + assertValid(); + + invalidated = true; + } + + /** {@inheritDoc} */ + @Override public boolean isNew() { + assertValid(); + + return isNew; + } + + /** + * @return Marshaled updates or empty map if no params. + * @throws IOException + */ + public Map<String, byte[]> binaryUpdatesMap() throws IOException { + final Map<String, Object> map = updatesMap; + + if (F.isEmpty(map)) + return Collections.emptyMap(); + + final Map<String, byte[]> res = new HashMap<>(map.size()); + + for (final Map.Entry<String, Object> entry : map.entrySet()) + res.put(entry.getKey(), marshal(entry.getValue())); + + return res; + } + + /** + * Unmarshal object. + * + * @param bytes Data. + * @param <T> Expected type. + * @return Unmarshaled object. + * @throws IOException If unarshaling failed. + */ + @Nullable private <T> T unmarshal(final byte[] bytes) throws IOException { + if (marshaller != null) { + try { + return marshaller.unmarshal(bytes, getClass().getClassLoader()); + } + catch (IgniteCheckedException e) { + throw new IOException(e); + } + } + + return null; + } + + /** + * Marshal object. + * + * @param obj Object to marshal. + * @return Binary data. + * @throws IOException If marshaling failed. + */ + @Nullable private byte[] marshal(final Object obj) throws IOException { + if (marshaller != null) { + try { + return marshaller.marshal(obj); + } + catch (IgniteCheckedException e) { + throw new IOException(e); + } + } + + return null; + } + + /** + * Marshal all attributes and save to serializable entity. + */ + public WebSessionEntity marshalAttributes() throws IOException { + final WebSessionEntity marshaled = new WebSessionEntity(getId(), entity.createTime(), accessTime, + maxInactiveInterval); + + for (final Map.Entry<String, Object> entry : attributes().entrySet()) + marshaled.attributes().put(entry.getKey(), marshal(entry.getValue())); + + return marshaled; + } + + /** + * @return Session attributes. + */ + private Map<String, Object> attributes() { + if (attrs == null) + attrs = new HashMap<>(); + + return attrs; + } + + /** + * @return Updates map. + */ + private Map<String, Object> updatesMap() { + if (updatesMap == null) + updatesMap = new HashMap<>(); + + return updatesMap; + } + + /** + * @return {@code True} if session wasn't invalidated. + */ + public boolean isValid() { + return !invalidated; + } + + /** + * Throw {@link IllegalStateException} if session was invalidated. + */ + private void assertValid() { + if (invalidated) + throw new IllegalStateException("Session was invalidated."); + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/test/java/org/apache/ignite/internal/websession/WebSessionSelfTest.java ---------------------------------------------------------------------- diff --git a/modules/web/src/test/java/org/apache/ignite/internal/websession/WebSessionSelfTest.java b/modules/web/src/test/java/org/apache/ignite/internal/websession/WebSessionSelfTest.java index 90cb132..a312b3c 100644 --- a/modules/web/src/test/java/org/apache/ignite/internal/websession/WebSessionSelfTest.java +++ b/modules/web/src/test/java/org/apache/ignite/internal/websession/WebSessionSelfTest.java @@ -18,8 +18,11 @@ package org.apache.ignite.internal.websession; import java.io.BufferedReader; +import java.io.Externalizable; import java.io.IOException; import java.io.InputStreamReader; +import java.io.ObjectInput; +import java.io.ObjectOutput; import java.net.URL; import java.net.URLConnection; import java.util.Random; @@ -39,6 +42,7 @@ import org.apache.ignite.internal.IgniteInternalFuture; import org.apache.ignite.internal.util.typedef.G; import org.apache.ignite.internal.util.typedef.X; import org.apache.ignite.internal.util.typedef.internal.U; +import org.apache.ignite.marshaller.Marshaller; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.eclipse.jetty.server.Server; @@ -56,6 +60,9 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { /** Servers count in load test. */ private static final int SRV_CNT = 3; + /** */ + private static boolean keepBinaryFlag = true; + /** * @return Name of the cache for this test. */ @@ -63,6 +70,13 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { return "partitioned"; } + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + super.beforeTest(); + + keepBinaryFlag = true; + } + /** * @throws Exception If failed. */ @@ -71,6 +85,15 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { } /** + * @throws Exception + */ + public void testSingleRequestNoKeepBinary() throws Exception { + keepBinaryFlag = false; + + testSingleRequest("/modules/core/src/test/config/websession/example-cache.xml"); + } + + /** * @throws Exception If failed. */ public void testSingleRequestMetaInf() throws Exception { @@ -99,7 +122,7 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { Ignite ignite = Ignition.start(srvCfg); try { - srv = startServer(TEST_JETTY_PORT, clientCfg, "client", new SessionCreateServlet()); + srv = startServer(TEST_JETTY_PORT, clientCfg, "client", keepBinaryFlag, new SessionCreateServlet()); URL url = new URL("http://localhost:" + TEST_JETTY_PORT + "/ignitetest/test"); @@ -158,7 +181,7 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { Server srv = null; try { - srv = startServer(TEST_JETTY_PORT, cfg, null, new SessionCreateServlet()); + srv = startServer(TEST_JETTY_PORT, cfg, null, keepBinaryFlag, new SessionCreateServlet()); URLConnection conn = new URL("http://localhost:" + TEST_JETTY_PORT + "/ignitetest/test").openConnection(); @@ -167,14 +190,36 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { try (BufferedReader rdr = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { String sesId = rdr.readLine(); - IgniteCache<String, HttpSession> cache = G.ignite().cache(getCacheName()); + if (!keepBinaryFlag) { + IgniteCache<String, HttpSession> cache = G.ignite().cache(getCacheName()); + + assertNotNull(cache); + + HttpSession ses = cache.get(sesId); + + assertNotNull(ses); - assertNotNull(cache); + assertEquals("val1", ses.getAttribute("key1")); + } + else { + final IgniteCache<String, WebSessionEntity> cache = G.ignite().cache(getCacheName()); + + assertNotNull(cache); + + final WebSessionEntity entity = cache.get(sesId); + + assertNotNull(entity); + + final byte[] data = entity.attributes().get("key1"); + + assertNotNull(data); + + final Marshaller marshaller = G.ignite().configuration().getMarshaller(); - HttpSession ses = cache.get(sesId); + final String val = marshaller.unmarshal(data, getClass().getClassLoader()); - assertNotNull(ses); - assertEquals("val1", ses.getAttribute("key1")); + assertEquals("val1", val); + } } } finally { @@ -194,7 +239,7 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { String cfg = "/modules/core/src/test/config/websession/spring-cache-" + (idx + 1) + ".xml"; srvs.set(idx, startServer( - TEST_JETTY_PORT + idx, cfg, "grid-" + (idx + 1), new RestartsTestServlet(sesIdRef))); + TEST_JETTY_PORT + idx, cfg, "grid-" + (idx + 1), keepBinaryFlag, new RestartsTestServlet(sesIdRef))); } final AtomicBoolean stop = new AtomicBoolean(); @@ -221,7 +266,7 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { String cfg = "/modules/core/src/test/config/websession/spring-cache-" + (idx + 1) + ".xml"; srv = startServer( - TEST_JETTY_PORT + idx, cfg, "grid-" + (idx + 1), new RestartsTestServlet(sesIdRef)); + TEST_JETTY_PORT + idx, cfg, "grid-" + (idx + 1), keepBinaryFlag, new RestartsTestServlet(sesIdRef)); assert srvs.compareAndSet(idx, null, srv); @@ -299,14 +344,18 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { * @param servlet Servlet. * @return Servlet container web context for this test. */ - protected WebAppContext getWebContext(@Nullable String cfg, @Nullable String gridName, HttpServlet servlet) { - WebAppContext ctx = new WebAppContext(U.resolveIgnitePath("modules/core/src/test/webapp").getAbsolutePath(), + protected WebAppContext getWebContext(@Nullable String cfg, @Nullable String gridName, + boolean keepBinaryFlag, HttpServlet servlet) { + final String path = keepBinaryFlag ? "modules/core/src/test/webapp" : "modules/web/src/test/webapp2"; + + WebAppContext ctx = new WebAppContext(U.resolveIgnitePath(path).getAbsolutePath(), "/ignitetest"); ctx.setInitParameter("IgniteConfigurationFilePath", cfg); ctx.setInitParameter("IgniteWebSessionsGridName", gridName); ctx.setInitParameter("IgniteWebSessionsCacheName", getCacheName()); ctx.setInitParameter("IgniteWebSessionsMaximumRetriesOnFail", "100"); + ctx.setInitParameter("IgniteWebSessionsKeepBinary", Boolean.toString(keepBinaryFlag)); ctx.addServlet(new ServletHolder(servlet), "/*"); @@ -323,11 +372,12 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { * @return Server. * @throws Exception In case of error. */ - private Server startServer(int port, @Nullable String cfg, @Nullable String gridName, HttpServlet servlet) + private Server startServer(int port, @Nullable String cfg, @Nullable String gridName, + boolean keepBinaryFlag, HttpServlet servlet) throws Exception { Server srv = new Server(port); - WebAppContext ctx = getWebContext(cfg, gridName, servlet); + WebAppContext ctx = getWebContext(cfg, gridName, keepBinaryFlag, servlet); srv.setHandler(ctx); @@ -359,6 +409,7 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { ses.setAttribute("checkCnt", 0); ses.setAttribute("key1", "val1"); ses.setAttribute("key2", "val2"); + ses.setAttribute("mkey", new TestObj("mval")); X.println(">>>", "Created session: " + ses.getId(), ">>>"); @@ -403,4 +454,60 @@ public class WebSessionSelfTest extends GridCommonAbstractTest { res.getWriter().flush(); } } + + /** + * + */ + private static class TestObj implements Externalizable { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + private String val; + + /** + * + */ + public TestObj() { + } + + /** + * @param val Value. + */ + public TestObj(final String val) { + this.val = val; + } + + /** {@inheritDoc} */ + @Override public void writeExternal(final ObjectOutput out) throws IOException { + U.writeString(out, val); + System.out.println("TestObj marshalled"); + } + + /** {@inheritDoc} */ + @Override public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { + // It must be unmarshalled only on client side. + if (keepBinaryFlag) + fail("Should not be unmarshalled"); + + val = U.readString(in); + System.out.println("TestObj unmarshalled"); + } + + /** {@inheritDoc} */ + @Override public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final TestObj testObj = (TestObj) o; + + return val != null ? val.equals(testObj.val) : testObj.val == null; + + } + + /** {@inheritDoc} */ + @Override public int hashCode() { + return val != null ? val.hashCode() : 0; + } + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/test/webapp2/META-INF/ignite-webapp-config.xml ---------------------------------------------------------------------- diff --git a/modules/web/src/test/webapp2/META-INF/ignite-webapp-config.xml b/modules/web/src/test/webapp2/META-INF/ignite-webapp-config.xml new file mode 100644 index 0000000..7fdd559 --- /dev/null +++ b/modules/web/src/test/webapp2/META-INF/ignite-webapp-config.xml @@ -0,0 +1,288 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + 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. +--> + +<!-- + Ignite Spring configuration file to startup grid cache. + + When starting a standalone Ignite node, you need to execute the following command: + {IGNITE_HOME}/bin/ignite.{bat|sh} examples/config/spring-cache.xml + + When starting Ignite from Java IDE, pass path to this file into Ignition: + Ignition.start("examples/config/spring-cache.xml"); +--> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:util="http://www.springframework.org/schema/util" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/util + http://www.springframework.org/schema/util/spring-util.xsd"> + <!-- + Configuration below demonstrates how to setup caches within grid nodes. + --> + <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"> + <property name="deploymentMode" value="SHARED"/> + + <!-- + For better performance set this property to false in case + peer deployment is not used. + Default value is false. + --> + <property name="peerClassLoadingEnabled" value="true"/> + + <!-- + Configure optimized marshaller. + --> + <property name="marshaller"> + <bean class="org.apache.ignite.marshaller.optimized.OptimizedMarshaller"> + <!-- + For better performance set this property to true in case + all marshalled classes implement java.io.Serializable. + Default value is true. + + Note, that it is recommended to implement java.io.Externalizable + instead of java.io.Serializable for smaller network footprint + and even better performance. + --> + <property name="requireSerializable" value="false"/> + </bean> + </property> + + <!-- Set to local host address just for examples. --> + <property name="localHost" value="127.0.0.1"/> + + <!-- Configure REST TCP server address. --> + <property name="connectorConfiguration"> + <bean class="org.apache.ignite.configuration.ConnectorConfiguration"> + <property name="host" value="127.0.0.1"/> + </bean> + </property> + + <!-- + Enable cache events. + --> + <property name="includeEventTypes"> + <util:constant static-field="org.apache.ignite.events.EventType.EVTS_CACHE"/> + </property> + + <property name="cacheConfiguration"> + <!-- + Specify list of cache configurations here. Any property from + CacheConfiguration interface can be configured here. + Note that absolutely all configuration properties are optional. + --> + <list> + <!-- + Partitioned cache example configuration (Atomic mode). + --> + <bean class="org.apache.ignite.configuration.CacheConfiguration"> + <property name="name" value="partitioned"/> + + <property name="cacheMode" value="PARTITIONED"/> + + <!-- Only atomic updates will be supported. --> + <property name="atomicityMode" value="ATOMIC"/> + + <!-- Enable primary sync write mode. --> + <property name="writeSynchronizationMode" value="PRIMARY_SYNC"/> + + <!-- Initial cache size. --> + <property name="startSize" value="1500000"/> + + <!-- + This shows how to configure number of backups. The below configuration + sets the number of backups to 1 (which is default). + --> + <property name="backups" value="1"/> + + <!-- Set synchronous rebalancing (default is asynchronous). --> + <property name="rebalanceMode" value="SYNC"/> + </bean> + + <!-- + Partitioned cache example configuration (Transactional mode). + --> + <bean class="org.apache.ignite.configuration.CacheConfiguration"> + <property name="name" value="partitioned_tx"/> + + <property name="cacheMode" value="PARTITIONED"/> + + <!-- Transactional updates supported. --> + <property name="atomicityMode" value="TRANSACTIONAL"/> + + <!-- Enable near cache to cache recently accessed data. --> + <property name="nearConfiguration"> + <bean class="org.apache.ignite.configuration.NearCacheConfiguration"/> + </property> + + <!-- Initial cache size. --> + <property name="startSize" value="1500000"/> + + <!-- + Setting this value will cause local node to wait for remote commits. + However, it's important to set it this way in the examples as we assert on + conditions that usually assume full completion of transactions on all nodes. + --> + <property name="writeSynchronizationMode" value="FULL_SYNC"/> + + <!-- + This shows how to configure number of backups. The below configuration + sets the number of backups to 1 (which is default). + --> + <property name="backups" value="1"/> + + <!-- Set synchronous rebalancing (default is asynchronous). --> + <property name="rebalanceMode" value="SYNC"/> + </bean> + + <!-- + Replicated cache example configuration. + --> + <bean class="org.apache.ignite.configuration.CacheConfiguration"> + <property name="name" value="replicated"/> + + <!-- + Setting this value will cause local node to wait for remote commits. + However, it's important to set it this way in the examples as we assert on + conditions that usually assume full completion of transactions on all nodes. + --> + <property name="writeSynchronizationMode" value="FULL_SYNC"/> + + <!-- REPLICATED cache mode. --> + <property name="cacheMode" value="REPLICATED"/> + + <!-- Set synchronous rebalancing (default is asynchronous). --> + <property name="rebalanceMode" value="SYNC"/> + + <!-- Initial cache size. --> + <property name="startSize" value="150000"/> + </bean> + + <!-- + Local cache example configuration. + --> + <bean class="org.apache.ignite.configuration.CacheConfiguration"> + <!-- Cache name is 'local'. --> + <property name="name" value="local"/> + + <!-- LOCAL cache mode. --> + <property name="cacheMode" value="LOCAL"/> + + <!-- Initial cache size. --> + <property name="startSize" value="150000"/> + </bean> + </list> + </property> + + <!-- + Uncomment this to provide TCP discovery SPI (Amazon EC2). + --> + <!-- + <property name="discoverySpi"> + <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi"> + <property name="ipFinder"> + <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.s3.TcpDiscoveryS3IpFinder"> + <property name="awsCredentials"> + <bean class="com.amazonaws.auth.BasicAWSCredentials"> + <constructor-arg value="YOUR_ACCESS_KEY_ID" /> + <constructor-arg value="YOUR_SECRET_ACCESS_KEY" /> + </bean> + </property> + <property name="bucketName" value="YOUR_BUCKET_NAME_IP_FINDER"/> + </bean> + </property> + <property name="heartbeatFrequency" value="2000"/> + </bean> + </property> + --> + + <!-- + Uncomment this to provide TCP discovery SPI (Local network). + + If path to shared file system is not explicitly provided, + then only local nodes will be able to discover each other. + --> + <!-- + <property name="discoverySpi"> + <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi"> + <property name="ipFinder"> + <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.sharedfs.TcpDiscoverySharedFsIpFinder"> + <property name="path" value="work/disco/tcp"/> + </bean> + </property> + </bean> + </property> + --> + + <!-- + TCP discovery SPI configuration with predefined addresses. + Use the addresses list to provide IP addresses of initial nodes in the grid + (at least one address must be provided). + + Note: + ===== + If running in distributed environment, you should change IP addresses to the actual IP addresses + of the servers on your network. Not all addresses need to be specified, only the addresses + of one or more servers which will always be started first. + --> + <property name="discoverySpi"> + <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi"> + <property name="ipFinder"> + <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder"> + <property name="addresses"> + <list> + <!-- + List all IP/port configurations that potentially + can be started first in examples. We are assuming + grid of size 10 or less. + --> + <value>127.0.0.1:47500</value> + <value>127.0.0.1:47501</value> + <value>127.0.0.1:47502</value> + <value>127.0.0.1:47503</value> + <value>127.0.0.1:47504</value> + <value>127.0.0.1:47505</value> + <value>127.0.0.1:47506</value> + <value>127.0.0.1:47507</value> + <value>127.0.0.1:47508</value> + <value>127.0.0.1:47509</value> + </list> + </property> + </bean> + <!-- + Uncomment this to provide IP finder using multicast for nodes discovery. + In addition to addresses received via multicast this finder can work with pre-configured + list of addresses. + --> + <!-- + <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder"> + <property name="addresses"> + <list> + <value>host1:port1</value> + <value>host2:port2</value> + </list> + </property> + </bean> + --> + </property> + </bean> + </property> + </bean> +</beans> http://git-wip-us.apache.org/repos/asf/ignite/blob/ee1cd486/modules/web/src/test/webapp2/WEB-INF/web.xml ---------------------------------------------------------------------- diff --git a/modules/web/src/test/webapp2/WEB-INF/web.xml b/modules/web/src/test/webapp2/WEB-INF/web.xml new file mode 100644 index 0000000..ad65447 --- /dev/null +++ b/modules/web/src/test/webapp2/WEB-INF/web.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + ~ Copyright (C) GridGain Systems. All Rights Reserved. + ~ _________ _____ __________________ _____ + ~ __ ____/___________(_)______ /__ ____/______ ____(_)_______ + ~ _ / __ __ ___/__ / _ __ / _ / __ _ __ `/__ / __ __ \ + ~ / /_/ / _ / _ / / /_/ / / /_/ / / /_/ / _ / _ / / / + ~ \____/ /_/ /_/ \_,__/ \____/ \__,_/ /_/ /_/ /_/ + --> + +<web-app xmlns="http://java.sun.com/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee + http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <!-- Declare listener for web sessions caching. --> + <listener> + <listener-class>org.apache.ignite.startup.servlet.ServletContextListenerStartup</listener-class> + </listener> + + <!-- Declare filter for web sessions caching. --> + <filter> + <filter-name>IgniteWebSessionsFilter</filter-name> + <filter-class>org.apache.ignite.cache.websession.WebSessionFilter</filter-class> + <init-param> + <param-name>IgniteWebSessionsKeepBinary</param-name> + <param-value>false</param-value> + </init-param> + </filter> + + <filter-mapping> + <filter-name>IgniteWebSessionsFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> +</web-app>
