Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport TransientCache expiration to 1.6.x #275

Merged
merged 1 commit into from Sep 2, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -32,11 +32,12 @@ public class DefaultStorageBroker implements StorageBroker {

private TransientCache transientCache;

@Deprecated
public DefaultStorageBroker(BlobStore blobStore) {
this(blobStore, new TransientCache(100, 1024));
}
public DefaultStorageBroker(BlobStore blobStore, TransientCache transientCache) {
this.blobStore = blobStore;

// @todo are these settings reasonable? should they be configurable?
transientCache = new TransientCache(100,1000);
}

public void addBlobStoreListener(BlobStoreListener listener){
Expand Down
Expand Up @@ -25,96 +25,139 @@
import org.geowebcache.mime.MimeType;
import org.geowebcache.storage.blobstore.file.FilePathGenerator;

import com.google.common.base.Preconditions;
import com.google.common.base.Ticker;

/**
* Non-thread safe Resource cache. Currently in-memory only.
*
* @author Ian Schneider <ischneider@opengeo.org>
* @author Kevin Smith, Boundless
*/
public class TransientCache {

private final int maxTiles;

private final int maxStorage;

private final long expireDelay;

private long currentStorage;

private Ticker ticker = Ticker.systemTicker();

/**
* A path generator that uses the key set as its key to build keys suitable for usage in the in
* memory transient cache
*/
private static FilePathGenerator keyGenerator = new FilePathGenerator("");

private Map<String, Resource> cache = new LinkedHashMap<String, Resource>() {
private Map<String, CachedResource> cache = new LinkedHashMap<String, CachedResource>() {

/** serialVersionUID */
private static final long serialVersionUID = -4106644240603796847L;

@Override
protected boolean removeEldestEntry(Entry<String, Resource> eldest) {
protected boolean removeEldestEntry(Entry<String, CachedResource> eldest) {
return removeEntries(eldest);
}

};

/**
* @deprecated Use {@link #TransientCache(int,int,long)} instead
*/
public TransientCache(int maxTiles, int maxStorageKB) {
this(maxTiles, maxStorageKB, 2000);
}

/**
*
* @param maxTiles Maximum number of tiles in cache
* @param maxStorageKB Maximum size of cached data in KiB
* @param expireDelay Duration for which the cached resource is valid in ms
*/
public TransientCache(int maxTiles, int maxStorageKB, long expireDelay) {
this.maxTiles = maxTiles;
this.maxStorage = maxStorageKB * 1024;
this.expireDelay = expireDelay;
}

/**
* Count of cached resources. May include expired resources not yet cleared.
* @return
*/
public int size() {
return cache.size();
}

/**
* The currently used storage. May include expired resources not yet cleared.
* @return
*/
public long storageSize() {
return currentStorage;
}

/**
* Store a resource
* @param key key to store the resource under
* @param r the resource to cache
*/
public void put(String key, Resource r) {
byte[] buf = new byte[(int) r.getSize()];
try {
r.getInputStream().read(buf);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
ByteArrayResource blob = new ByteArrayResource(buf);
CachedResource blob = new CachedResource(new ByteArrayResource(buf));
currentStorage += r.getSize();
cache.put(key, blob);
}


/**
* Retrieve a resource
* @param key
* @return The resource cached under the given key, or null if no resource is cached.
*/
public Resource get(String key) {
Resource cached = cache.get(key);
CachedResource cached = cache.get(key);
if (cached != null) {
cache.remove(key);
currentStorage -= cached.getSize();
currentStorage -= cached.content.getSize();

if( cached.time+expireDelay < currentTime() ) {
return null;
} else {
return cached.content;
}
}
return cached;
return null;
}

/**
* Loop through elements, removing and recompute currentStorage size
* A timestamp in milliseconds
* @return
*/
private void clean() {
// iterator returns items in order added so oldest items are first
Iterator<Resource> items = cache.values().iterator();
long storage = currentStorage;
while (items.hasNext()) {
Resource r = items.next();
storage -= r.getSize();
if (storage < maxStorage) {
break;
} else {
items.remove();
}
}
currentStorage = storage;
protected long currentTime() {
return ticker.read()/1000;
}

private boolean removeEntries(Entry<String, Resource> eldest) {
boolean remove = false;
// not sure if we can do both at the same time?
if (currentStorage > maxStorage) {
clean();
} else if (cache.size() > maxTiles) {
remove = true;

// Gets called by overridden LinkedHashMap.removeEldestEntry
private boolean removeEntries(Entry<String, CachedResource> eldest) {
// iterator returns items in order added so oldest items are first
Iterator<CachedResource> items = cache.values().iterator();
while (items.hasNext() && (currentStorage>maxStorage || cache.size()>maxTiles)) {
CachedResource r = items.next();
currentStorage -= r.content.getSize();
items.remove();
}
return remove;
assert currentStorage<=maxStorage;
assert currentStorage>=0;
assert cache.size()<=maxStorage;

return false;
}

public static String computeTransientKey(TileObject tile) {
Expand All @@ -126,4 +169,27 @@ public static String computeTransientKey(TileObject tile) {
}
}

private class CachedResource {
Resource content;
long time;

public CachedResource(Resource content, long time) {
super();
this.content = content;
this.time = time;
}

public CachedResource(Resource content) {
this(content, currentTime());
}
}

/**
* Set a time source for computing expiry.
* @param ticker
*/
public void setTicker(Ticker ticker) {
Preconditions.checkNotNull(ticker);
this.ticker=ticker;
}
}
Expand Up @@ -78,6 +78,7 @@ private StorageBroker resetAndPrepStorageBroker() throws Exception {
}

BlobStore blobStore = new FileBlobStore(blobPath);
TransientCache transCache = new TransientCache(100, 1024, 2000);

StorageBroker sb = new DefaultStorageBroker(blobStore);

Expand Down
@@ -0,0 +1,163 @@
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.geowebcache.storage;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

import java.io.InputStream;

import org.geowebcache.io.ByteArrayResource;
import org.geowebcache.io.Resource;
import org.junit.Before;
import org.junit.Test;

import com.google.common.base.Ticker;

/**
*
* @author Kevin Smith, Boundless
*
*/
public class TransientCacheTest {

TransientCache transCache;
TestTicker ticker;

final public static long EXPIRE_TIME = 2000;
final public static int MAX_TILES = 5;
final public static int MAX_SPACE_KiB = 5;

@Before
public void setUp() throws Exception {
transCache = new TransientCache(MAX_TILES, MAX_SPACE_KiB, EXPIRE_TIME);
ticker = new TestTicker(System.nanoTime());
transCache.setTicker(ticker);
}

@Test
public void testHit() throws Exception {
Resource r = new ByteArrayResource(new byte[]{1,2,3});

transCache.put("foo", r);

ticker.advanceMilli(EXPIRE_TIME-1);
Resource result = transCache.get("foo");
assertThat(result, notNullValue());
assertThat(r.getLastModified(), equalTo(r.getLastModified()));
try (InputStream is = result.getInputStream();){
assertThat(is.read(), equalTo(1));
assertThat(is.read(), equalTo(2));
assertThat(is.read(), equalTo(3));
assertThat(is.read(), equalTo(-1));
}
}

@Test
public void testRemoveOnHit() throws Exception {
Resource r = new ByteArrayResource(new byte[]{1,2,3});

transCache.put("foo", r);

ticker.advanceMilli(EXPIRE_TIME-1);

transCache.get("foo"); // Hit

Resource result = transCache.get("foo");
assertThat(result, nullValue()); // Should have been cleared
}

@Test
public void testRemoveOnExpire() throws Exception {
Resource r = new ByteArrayResource(new byte[]{1,2,3});

transCache.put("foo", r);

ticker.advanceMilli(EXPIRE_TIME+1);

Resource result = transCache.get("foo");
assertThat(result, nullValue()); // Should have expired
}

@Test
public void testRemoveWhenMaxTiles() throws Exception {

for (byte i = 0; i<MAX_TILES; i++) {
Resource r = new ByteArrayResource(new byte[]{(byte)(i+1),(byte)(i+2),(byte)(i+3)});
transCache.put("foo"+i, r);
assertThat(transCache.size(), is(i+1));
}
assertThat(transCache.storageSize(), is((long)MAX_TILES*3));
Resource r = new ByteArrayResource(new byte[]{(byte)(MAX_TILES+1),(byte)(MAX_TILES+2)});
transCache.put("foo"+MAX_TILES, r);
assertThat(transCache.size(), is(MAX_TILES));
assertThat(transCache.storageSize(), is((long)MAX_TILES*3-1)); // remove a 3 byte and add a 2 byte

ticker.advanceMilli(1);

Resource result1 = transCache.get("foo0");
assertThat(result1, nullValue()); // Should have expired
Resource result2 = transCache.get("foo1");
assertThat(result2, notNullValue()); // Should still be cached
}

@Test
public void testRemoveWhenMaxSpace() throws Exception {

for (long i = 0; i<MAX_SPACE_KiB; i++) {
Resource r = new ByteArrayResource(new byte[i==0?1023:1024]); // make the first one 1 byte less than a KiB
transCache.put("foo"+i, r);
assertThat(transCache.storageSize(), is((i+1)*1024-1)); // 1 KiB per resource, less a byte for the first
ticker.advanceMilli(1);
}
assertThat(transCache.storageSize(), is((long)MAX_SPACE_KiB*1024-1)); // 1 KiB per resource, less a byte for the first
assertThat(transCache.size(), is(MAX_SPACE_KiB));
Resource r = new ByteArrayResource(new byte[2]); // 2 bytes will go over the maximum
transCache.put("foo"+MAX_SPACE_KiB, r);
assertThat(transCache.storageSize(), is((long)(MAX_SPACE_KiB-1)*1024+2)); // 1 KiB for each of the resources except the first should be removed, and the last is only 2 bytes.
assertThat(transCache.size(), is(MAX_SPACE_KiB));

ticker.advanceMilli(1);

Resource result1 = transCache.get("foo0");
assertThat(result1, nullValue()); // Should have expired
Resource result2 = transCache.get("foo1");
assertThat(result2, notNullValue()); // Should still be cached
}

static private class TestTicker extends Ticker {
long time;


public TestTicker(long startAt) {
super();
this.time = startAt;
}

@Override
public long read() {
// TODO Auto-generated method stub
return time;
}

public void advanceMilli(long millis) {
advanceNano(millis*1000);
}
public void advanceNano(long nanos) {
time+=nanos;
}
}
}