/* * 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.wicket.pageStore; import java.util.Iterator; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import org.apache.wicket.util.lang.Args; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Facade for {@link IDataStore} that does the actual saving in worker thread. * <p> * Creates an {@link Entry} for each triple (sessionId, pageId, data) and puts it in * {@link #entries} queue if there is room. Acts as producer.<br/> * Later {@link PageSavingRunnable} reads in blocking manner from {@link #entries} and saves each * entry. Acts as consumer. * </p> * It starts only one instance of {@link PageSavingRunnable} because all we need is to make the page * storing asynchronous. We don't want to write concurrently in the wrapped {@link IDataStore}, * though it may happen in the extreme case when the queue is full. These cases should be avoided. * * @author Matej Knopp */ public class AsynchronousDataStore implements IDataStore { /** Log for reporting. */ private static final Logger log = LoggerFactory.getLogger(AsynchronousDataStore.class); /** * The time to wait when adding an {@link Entry} into the entries. In millis. */ private static final long OFFER_WAIT = 30L; /** * The time to wait for an entry to save with the wrapped {@link IDataStore}. In millis. */ private static final long POLL_WAIT = 1000L; /** * The page saving thread. */ private final Thread pageSavingThread; /** * The wrapped {@link IDataStore} that actually stores that pages */ private final IDataStore dataStore; /** * The queue where the entries which have to be saved are temporary stored */ private final BlockingQueue<Entry> entries; /** * A map 'sessionId:::pageId' -> {@link Entry}. Used for fast retrieval of {@link Entry}s which * are not yet stored by the wrapped {@link IDataStore} */ private final ConcurrentMap<String, Entry> entryMap; /** * Construct. * * @param dataStore * the wrapped {@link IDataStore} that actually saved the data * @param capacity * the capacity of the queue that delays the saving */ public AsynchronousDataStore(final IDataStore dataStore, final int capacity) { this.dataStore = dataStore; entries = new LinkedBlockingQueue<>(capacity); entryMap = new ConcurrentHashMap<>(); PageSavingRunnable savingRunnable = new PageSavingRunnable(dataStore, entries, entryMap); pageSavingThread = new Thread(savingRunnable, "Wicket-AsyncDataStore-PageSavingThread"); pageSavingThread.setDaemon(true); pageSavingThread.start(); } @Override public void destroy() { if (pageSavingThread.isAlive()) { pageSavingThread.interrupt(); try { pageSavingThread.join(); } catch (InterruptedException e) { log.error(e.getMessage(), e); } } dataStore.destroy(); } /** * Little helper * * @param sessionId * @param id * @return Entry */ private Entry getEntry(final String sessionId, final int id) { return entryMap.get(getKey(sessionId, id)); } @Override public byte[] getData(final String sessionId, final int id) { Entry entry = getEntry(sessionId, id); if (entry != null) { log.debug( "Returning the data of a non-stored entry with sessionId '{}' and pageId '{}'", sessionId, id); return entry.data; } byte[] data = dataStore.getData(sessionId, id); log.debug("Returning the data of a stored entry with sessionId '{}' and pageId '{}'", sessionId, id); return data; } @Override public boolean isReplicated() { return dataStore.isReplicated(); } @Override public void removeData(final String sessionId, final int id) { String key = getKey(sessionId, id); if (key != null) { Entry entry = entryMap.remove(key); if (entry != null) { entries.remove(entry); } } dataStore.removeData(sessionId, id); } @Override public void removeData(final String sessionId) { for (Iterator<Entry> itor = entries.iterator(); itor.hasNext();) { Entry entry = itor.next(); if (entry != null) // this check is not needed in JDK6 { String entrySessionId = entry.sessionId; if (sessionId.equals(entrySessionId)) { entryMap.remove(getKey(entry)); itor.remove(); } } } dataStore.removeData(sessionId); } /** * Save the entry in the queue if there is a room or directly pass it to the wrapped * {@link IDataStore} if there is no such * * @see org.apache.wicket.pageStore.IDataStore#storeData(java.lang.String, int, byte[]) */ @Override public void storeData(final String sessionId, final int id, final byte[] data) { Entry entry = new Entry(sessionId, id, data); String key = getKey(entry); entryMap.put(key, entry); try { boolean added = entries.offer(entry, OFFER_WAIT, TimeUnit.MILLISECONDS); if (added == false) { log.debug("Storing synchronously page with id '{}' in session '{}'", id, sessionId); entryMap.remove(key); dataStore.storeData(sessionId, id, data); } } catch (InterruptedException e) { log.error(e.getMessage(), e); entryMap.remove(key); dataStore.storeData(sessionId, id, data); } } /** * * @param pageId * @param sessionId * @return generated key */ private static String getKey(final String sessionId, final int pageId) { return pageId + ":::" + sessionId; } /** * * @param entry * @return generated key */ private static String getKey(final Entry entry) { return getKey(entry.sessionId, entry.pageId); } /** * The structure used for an entry in the queue */ private static class Entry { private final String sessionId; private final int pageId; private final byte data[]; public Entry(final String sessionId, final int pageId, final byte data[]) { this.sessionId = Args.notNull(sessionId, "sessionId"); this.pageId = pageId; this.data = Args.notNull(data, "data"); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + pageId; result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Entry other = (Entry)obj; if (pageId != other.pageId) return false; if (sessionId == null) { if (other.sessionId != null) return false; } else if (!sessionId.equals(other.sessionId)) return false; return true; } @Override public String toString() { return "Entry [sessionId=" + sessionId + ", pageId=" + pageId + "]"; } } /** * The thread that acts as consumer of {@link Entry}ies */ private static class PageSavingRunnable implements Runnable { private static final Logger log = LoggerFactory.getLogger(PageSavingRunnable.class); private final BlockingQueue<Entry> entries; private final ConcurrentMap<String, Entry> entryMap; private final IDataStore dataStore; private PageSavingRunnable(IDataStore dataStore, BlockingQueue<Entry> entries, ConcurrentMap<String, Entry> entryMap) { this.dataStore = dataStore; this.entries = entries; this.entryMap = entryMap; } @Override public void run() { while (!Thread.interrupted()) { Entry entry = null; try { entry = entries.poll(POLL_WAIT, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (entry != null) { log.debug("Saving asynchronously: {}...", entry); dataStore.storeData(entry.sessionId, entry.pageId, entry.data); entryMap.remove(getKey(entry)); } } } } @Override public final boolean canBeAsynchronous() { // should not wrap in another AsynchronousDataStore return false; } }