/* * 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.io.Serializable; 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.page.IManageablePage; import org.apache.wicket.util.lang.Args; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Facade for {@link IPageStore} that does the actual saving in worker thread. * <p> * Creates an {@link Entry} for each double (sessionId, page) 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 IPageStore}, * though it may happen in the extreme case when the queue is full. These cases should be avoided. * * Based on AsynchronousDataStore (@author Matej Knopp). * * @author manuelbarzi */ public class AsynchronousPageStore implements IPageStore { /** Log for reporting. */ private static final Logger log = LoggerFactory.getLogger(AsynchronousPageStore.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 IPageStore} . In millis. */ private static final long POLL_WAIT = 1000L; /** * The page saving thread. */ private final Thread pageSavingThread; /** * The wrapped {@link IPageStore} that actually stores that pages */ private final IPageStore delegate; /** * 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 IPageStore} */ private final ConcurrentMap<String, Entry> entryMap; /** * Construct. * * @param delegate * the wrapped {@link IPageStore} that actually saved the page * @param capacity * the capacity of the queue that delays the saving */ public AsynchronousPageStore(final IPageStore delegate, final int capacity) { this.delegate = Args.notNull(delegate, "delegate"); entries = new LinkedBlockingQueue<>(capacity); entryMap = new ConcurrentHashMap<>(); PageSavingRunnable savingRunnable = new PageSavingRunnable(delegate, entries, entryMap); pageSavingThread = new Thread(savingRunnable, "Wicket-AsyncPageStore-PageSavingThread"); pageSavingThread.setDaemon(true); pageSavingThread.start(); } /** * Little helper * * @param sessionId * @param pageId * @return Entry */ private Entry getEntry(final String sessionId, final int pageId) { return entryMap.get(getKey(sessionId, pageId)); } /** * * @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.page.getPageId()); } /** * The structure used for an entry in the queue */ private static class Entry { private final String sessionId; private final IManageablePage page; public Entry(final String sessionId, final IManageablePage page) { this.sessionId = Args.notNull(sessionId, "sessionId"); this.page = Args.notNull(page, "page"); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + page.getPageId(); result = prime * result + 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 (page.getPageId() != other.page.getPageId()) return false; if (!sessionId.equals(other.sessionId)) return false; return true; } @Override public String toString() { return "Entry [sessionId=" + sessionId + ", pageId=" + page.getPageId() + "]"; } } /** * 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 IPageStore delegate; private PageSavingRunnable(IPageStore delegate, BlockingQueue<Entry> entries, ConcurrentMap<String, Entry> entryMap) { this.delegate = delegate; 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); delegate.storePage(entry.sessionId, entry.page); entryMap.remove(getKey(entry)); } } } } @Override public void destroy() { if (pageSavingThread.isAlive()) { pageSavingThread.interrupt(); try { pageSavingThread.join(); } catch (InterruptedException e) { log.error(e.getMessage(), e); } } delegate.destroy(); } @Override public IManageablePage getPage(String sessionId, int pageId) { Entry entry = getEntry(sessionId, pageId); if (entry != null) { log.debug( "Returning the page of a non-stored entry with session id '{}' and page id '{}'", sessionId, pageId); return entry.page; } IManageablePage page = delegate.getPage(sessionId, pageId); log.debug("Returning the page of a stored entry with session id '{}' and page id '{}'", sessionId, pageId); return page; } @Override public void removePage(String sessionId, int pageId) { String key = getKey(sessionId, pageId); if (key != null) { Entry entry = entryMap.remove(key); if (entry != null) { entries.remove(entry); } } delegate.removePage(sessionId, pageId); } @Override public void storePage(String sessionId, IManageablePage page) { Entry entry = new Entry(sessionId, page); String key = getKey(entry); entryMap.put(key, entry); try { if (entries.offer(entry, OFFER_WAIT, TimeUnit.MILLISECONDS)) { log.debug("Offered for storing asynchronously page with id '{}' in session '{}'", page.getPageId(), sessionId); } else { log.debug("Storing synchronously page with id '{}' in session '{}'", page.getPageId(), sessionId); entryMap.remove(key); delegate.storePage(sessionId, page); } } catch (InterruptedException e) { log.error(e.getMessage(), e); entryMap.remove(key); delegate.storePage(sessionId, page); } } @Override public void unbind(String sessionId) { delegate.unbind(sessionId); } @Override public Serializable prepareForSerialization(String sessionId, Serializable page) { return delegate.prepareForSerialization(sessionId, page); } @Override public Object restoreAfterSerialization(Serializable serializable) { return delegate.restoreAfterSerialization(serializable); } @Override public IManageablePage convertToPage(Object page) { return delegate.convertToPage(page); } @Override public boolean canBeAsynchronous() { // should not wrap in another AsynchronousPageStore return false; } }