4

I am dealing with a custom implementation for wicket session store, data store, page store. I have cu cluster wicket and make it work in the following situation:

There are 2 nodes in the cluster, node one fails and the user should be able to continue the flow without noticing, the pages a statefull, with a lot of ajax requests. For now I'm storing the wicket session in a custom storage over rmi, and I'm trying to extend the DiskPageStore. The new challenge is SessionEntry inner class, it is still hold by a ConcurrentMap.

My question is: Has anyone done this before? Do you have any suggestions on how to accomplish this?

BigJump
  • 15,561
  • 3
  • 31
  • 29

1 Answers1

5

My suggestion is forget about DiskPageStore and SessionEntry in your situation. The ConcurrentMap you mentioned is held in the heap locally. Once one of the nodes fails, there is no way to get access to its ConcurrentMap, and Wicket resources referred to from the ConcurrentMap will be impossible to be released.

Therefore, in a clustered environment, you need to cluster the Wicket page store. Page versions can be expired based on certain policy, or deliberately removed when their corresponding session expires.

I've enabled web session and data store clustering for Apache Wicket used in an enterprise web application in production, and it has been working very well. The software I use are:

  • JDK 1.8.0_60
  • Apache Tomcat 8.0.33 (Tomcat 7 works too)
  • Wicket 6.16 (versions 6.22.0 and 7.2.0 should also work)
  • Apache Ignite 1.7.0
  • Load balancer: Crossroads
  • Ubuntu 14.04.1

The idea is use Apache Ignite for web session clustering, and it is pretty straightforward following its instructions for Web Session Clustering.

Once I got the web session clustered, I then put the data store (which includes the page store already) into the Ignite distributed data grid, while at the same time I disabled the Wicket application scoped cache (so as to make sure all data is clustered). Take a look at the documentation on Wicket's page store to find out how to configure the data store.

Alternatively you should be able to use Wicket HttpSessionDataStore to put the data store into the session. As the session is clustered, the data store is clustered automatically. But this approach does not work compatibly with Apache Ignite for me. So I use my own implementation of the IDataStore interface, which puts the data store into the Ignite distributed data grid. See below the implementation.

import java.util.concurrent.TimeUnit;
import javax.cache.expiry.Duration;
import javax.cache.expiry.TouchedExpiryPolicy;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.Ignition;
import org.apache.ignite.cache.CacheMemoryMode;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.eviction.lru.LruEvictionPolicy;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.wicket.pageStore.IDataStore;
import org.apache.wicket.pageStore.memory.IDataStoreEvictionStrategy;
import org.apache.wicket.pageStore.memory.PageTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class IgniteDataStore implements IDataStore {

    private static final Logger log = LoggerFactory.getLogger(IgniteDataStore.class);

    private final IDataStoreEvictionStrategy evictionStrategy;
    private Ignite ignite;
    IgniteCache<String, PageTable> igniteCache;

    public IgniteDataStore(IDataStoreEvictionStrategy evictionStrategy) {
        this.evictionStrategy = evictionStrategy;

        CacheConfiguration<String, PageTable> cacheCfg = new CacheConfiguration<String, PageTable>("wicket-data-store");
        cacheCfg.setCacheMode(CacheMode.PARTITIONED);
        cacheCfg.setBackups(1);

        cacheCfg.setMemoryMode(CacheMemoryMode.OFFHEAP_VALUES);
        cacheCfg.setOffHeapMaxMemory(2 * 1024L * 1024L * 1024L); // 2 Gigabytes.

        cacheCfg.setEvictionPolicy(new LruEvictionPolicy<String, PageTable>(10000));

        cacheCfg.setExpiryPolicyFactory(TouchedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, 14400)));
        log.info("IgniteDataStore timeout is set to 14400 seconds.");

        ignite = Ignition.ignite();
        igniteCache = ignite.getOrCreateCache(cacheCfg);
    }

    @Override
    public synchronized byte[] getData(String sessionId, int id) {
        PageTable pageTable = getPageTable(sessionId, false);
        byte[] pageAsBytes = null;
        if (pageTable != null) {
            pageAsBytes = pageTable.getPage(id);
        }
        return pageAsBytes;
    }

    @Override
    public synchronized void removeData(String sessionId, int id) {
        PageTable pageTable = getPageTable(sessionId, false);
        if (pageTable != null) {
            pageTable.removePage(id);
        }
    }

    @Override
    public synchronized void removeData(String sessionId) {
        PageTable pageTable = getPageTable(sessionId, false);
        if (pageTable != null) {
            pageTable.clear();
        }
        igniteCache.remove(sessionId);
    }

    @Override
    public synchronized void storeData(String sessionId, int id, byte[] data) {
        PageTable pageTable = getPageTable(sessionId, true);
        if (pageTable != null) {
            pageTable.storePage(id, data);
            evictionStrategy.evict(pageTable);
            igniteCache.put(sessionId, pageTable);
        } else {
            log.error("Cannot store the data for page with id '{}' in session with id '{}'", id, sessionId);
        }
    }

    @Override
    public synchronized void destroy() {
        igniteCache.clear();
    }

    @Override
    public boolean isReplicated() {
        return true;
    }

    @Override
    public boolean canBeAsynchronous() {
        return false;
    }

    private PageTable getPageTable(String sessionId, boolean create) {  
        if (igniteCache.containsKey(sessionId)) {
            return igniteCache.get(sessionId);
        }

        if (!create) {
            return null;
        }

        PageTable pageTable = new PageTable();
        igniteCache.put(sessionId, pageTable);
        return pageTable;
    }
}

Hope it helps.

Yuci
  • 27,235
  • 10
  • 114
  • 113
  • I'm going to try this with AWS DynamoDB. How do you "disable the Wicket application scoped cache" ? – Hendy Irawan Aug 01 '17 at 07:08
  • 1
    In the init() method of your WebApplication class, add this (for Wicket 6, and probably similar or the same in Wicket 7): this.getStoreSettings().setInmemoryCacheSize(0); And let me know how you get on with Wicket/Ignite on AWS DynamoDB. Interested to know. Cheers. – Yuci Aug 01 '17 at 10:03
  • It works beautifully (https://satukancinta.com) although with a very strict limitation of < 400 KB sessions (about 10 Wicket pages or so). Other than that I'm happy. :) – Hendy Irawan Aug 08 '17 at 00:26
  • Good to hear that. By the way, wonder what the limiting factors are for the "strict limitation of < 400 KB sessions (about 10 Wicket pages or so)". I thought these are configurable in Ignite, (while the number of Wicket pages is configured in Wicket of course). Cheers. – Yuci Aug 08 '17 at 10:03
  • I'm using DynamoDB so it's a limitation from DynamoDB. – Hendy Irawan Aug 08 '17 at 11:11
  • 1
    I have to say I'm not so happy now, not because of DynamoDB itself but because of the costs. Since session is pretty read **and** write intensive, it uses a lot of provisioning capacity (> 50 both read and write). I'm thinking of moving to ElastiCache (i.e. memcached), which seems to be cheaper than DynamoDB for my use case (and faster!), or even PostgreSQL (where storage space is abundant)... Both alternatives do not have session size limitation as DynamoDB. – Hendy Irawan Aug 10 '17 at 01:51
  • Why not consider Apache Ignite, which has native integration with Amazon AWS: https://ignite.apache.org/features/deploy.html. Ignite manages sessions in an in-memory approach, and it runs perfectly well with Apache Wicket for our high-profile web production. – Yuci Aug 10 '17 at 13:04
  • 1
    I'm not exactly comfortable managing infrastructure. If someone provides a hosted, fully managed Ignite for a fraction of the price of ElastiCache, then I'm definitely interested. – Hendy Irawan Aug 10 '17 at 16:28