Skip to content

Commit 55e50dc

Browse files
authored
Merge pull request #16 from zeroae/f/disk-lru-cache
Add DiskLRU Based cache for Gate responses
2 parents 83d42bf + af81746 commit 55e50dc

File tree

4 files changed

+145
-25
lines changed

4 files changed

+145
-25
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
implementation 'uk.ac.gate.plugins:format-json:8.7'
1717
implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
1818
implementation 'com.amazonaws:aws-lambda-java-events:2.2.9'
19+
implementation 'com.jakewharton:disklrucache:2.0.2'
1920

2021
runtimeOnly 'com.amazonaws:aws-lambda-java-log4j2:1.2.0'
2122

src/main/java/co/zeroae/gate/App.java

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77

88
import gate.*;
99
import gate.corpora.export.GATEJsonExporter;
10-
import gate.creole.ExecutionException;
1110
import gate.creole.ResourceInstantiationException;
1211
import gate.util.GateException;
1312
import gate.util.persistence.PersistenceManager;
13+
14+
import com.jakewharton.disklrucache.DiskLruCache;
15+
1416
import org.apache.log4j.LogManager;
1517
import org.apache.log4j.Logger;
18+
import org.codehaus.httpcache4j.util.Hex;
1619

17-
import java.io.ByteArrayOutputStream;
18-
import java.io.File;
19-
import java.io.IOException;
20+
import javax.xml.stream.XMLStreamException;
21+
import java.io.*;
2022
import java.net.URL;
23+
import java.security.MessageDigest;
24+
import java.security.NoSuchAlgorithmException;
2125
import java.util.HashMap;
2226
import java.util.HashSet;
2327
import java.util.Objects;
@@ -36,38 +40,94 @@ public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatew
3640
}
3741
}
3842

43+
private static final String GATE_APP_NAME = System.getenv("GATE_APP_NAME");
44+
private static final String CACHE_DIR = System.getenv().getOrDefault(
45+
"CACHE_DIR_PREFIX", "/tmp/lru/" + GATE_APP_NAME );
46+
private static final double CACHE_DIR_USAGE = .9;
47+
3948
private static final Logger logger = LogManager.getLogger(App.class);
4049
private static final CorpusController application = loadApplication();
4150
private static final GATEJsonExporter gateJsonExporter = new GATEJsonExporter();
4251

52+
53+
private static final DiskLruCache cache = initializeCache();
54+
4355
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, final Context context) {
4456
final String responseType = input.getHeaders().getOrDefault("Accept", "application/xml");
4557
final APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
4658
.withHeaders(new HashMap<>());
4759
response.getHeaders().put("Content-Type", "text/plain");
4860

4961
try {
50-
final Document doc = Factory.newDocument(input.getBody());
51-
final Corpus corpus = application.getCorpus();
52-
corpus.add(doc);
62+
final String bodyDigest = computeMessageDigest(input.getBody());
63+
response.getHeaders().put("x-zae-gate-cache", "HIT");
64+
final Document doc = cacheComputeIfNull(
65+
bodyDigest,
66+
() -> {
67+
final Document rv = Factory.newDocument(input.getBody());
68+
final Corpus corpus = application.getCorpus();
69+
corpus.add(rv);
70+
try {
71+
application.execute();
72+
} finally {
73+
corpus.clear();
74+
}
75+
response.getHeaders().put("x-zae-gate-cache", "MISS");
76+
return rv;
77+
}
78+
);
5379
try {
54-
application.execute();
5580
response.getHeaders().put("Content-Type", responseType);
5681
return response.withBody(export(doc, responseType)).withStatusCode(200);
5782
} finally {
58-
corpus.clear();
5983
Factory.deleteResource(doc);
6084
}
61-
} catch (ExecutionException e) {
85+
} catch (GateException e) {
6286
logger.error(e);
6387
return response.withBody(e.getMessage()).withStatusCode(400);
6488
} catch (IOException e) {
6589
logger.error(e);
6690
return response.withBody(e.getMessage()).withStatusCode(406);
67-
} catch (ResourceInstantiationException e) {
91+
}
92+
}
93+
94+
private Document cacheComputeIfNull(String key, TextProcessor processor) throws GateException {
95+
try {
96+
final DiskLruCache.Snapshot snapshot = cache.get(key);
97+
if (snapshot == null) {
98+
final Document doc = processor.process();
99+
try {
100+
DiskLruCache.Editor editor = cache.edit(key);
101+
editor.set(0, doc.toXml());
102+
editor.commit();
103+
} catch (IOException e) {
104+
logger.warn(e);
105+
}
106+
return doc;
107+
} else {
108+
try {
109+
return Utils.xmlToDocument(new InputStreamReader(snapshot.getInputStream(0)));
110+
} catch (ResourceInstantiationException | XMLStreamException e) {
111+
logger.warn(e);
112+
cache.remove(key);
113+
return processor.process();
114+
}
115+
}
116+
} catch (IOException e) {
68117
logger.warn(e);
69-
return response.withBody(e.getMessage()).withStatusCode(400);
118+
return processor.process();
119+
}
120+
}
121+
122+
private String computeMessageDigest(String text) {
123+
final String sha256;
124+
try {
125+
final MessageDigest md = MessageDigest.getInstance("SHA-256");
126+
sha256 = Hex.encode(md.digest(text.getBytes()));
127+
} catch (NoSuchAlgorithmException e) {
128+
throw new RuntimeException(e);
70129
}
130+
return sha256;
71131
}
72132

73133
/**
@@ -97,7 +157,7 @@ private String export(Document doc, String responseType) throws IOException {
97157

98158
private static CorpusController loadApplication() {
99159
try {
100-
final String gappResourcePah = System.getenv("GATE_APP_NAME") + "/application.xgapp";
160+
final String gappResourcePah = GATE_APP_NAME + "/application.xgapp";
101161
final URL gappUrl = App.class.getClassLoader().getResource(gappResourcePah);
102162
final File gappFile = new File(Objects.requireNonNull(gappUrl).getFile());
103163
final CorpusController rv =
@@ -109,4 +169,26 @@ private static CorpusController loadApplication() {
109169
throw new RuntimeException(e);
110170
}
111171
}
172+
173+
private static DiskLruCache initializeCache() {
174+
File cacheDir = new File(CACHE_DIR);
175+
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
176+
throw new RuntimeException("Unable to create cache directory '" + cacheDir.getName() + "'.");
177+
}
178+
for (File file: Objects.requireNonNull(cacheDir.listFiles())) file.delete();
179+
try {
180+
long usableSpace = (long) (cacheDir.getUsableSpace()*CACHE_DIR_USAGE);
181+
return DiskLruCache.open(cacheDir,
182+
1,
183+
1,
184+
usableSpace);
185+
} catch (IOException e) {
186+
throw new RuntimeException(e);
187+
}
188+
}
189+
190+
private interface TextProcessor {
191+
Document process() throws GateException;
192+
}
193+
112194
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package co.zeroae.gate;
2+
3+
import gate.Document;
4+
import gate.Factory;
5+
import gate.creole.ResourceInstantiationException;
6+
7+
import javax.xml.stream.XMLInputFactory;
8+
import javax.xml.stream.XMLStreamException;
9+
import javax.xml.stream.XMLStreamReader;
10+
import java.io.InputStream;
11+
import java.io.InputStreamReader;
12+
import java.io.Reader;
13+
14+
public class Utils {
15+
/**
16+
*
17+
* @param gateXMLReader
18+
* @return
19+
* @throws ResourceInstantiationException if Document
20+
* @throws XMLStreamException
21+
*/
22+
static Document xmlToDocument(Reader gateXMLReader) throws ResourceInstantiationException, XMLStreamException {
23+
final Document doc = Factory.newDocument("");
24+
XMLStreamReader reader;
25+
reader = XMLInputFactory.newFactory().createXMLStreamReader(gateXMLReader);
26+
do {
27+
reader.next();
28+
} while(reader.getEventType() != XMLStreamReader.START_ELEMENT);
29+
gate.corpora.DocumentStaxUtils.readGateXmlDocument(reader, doc);
30+
return doc;
31+
}
32+
}

src/test/java/co/zeroae/gate/AppTest.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55
import com.fasterxml.jackson.core.JsonFactory;
66
import com.fasterxml.jackson.core.JsonParser;
77
import gate.Document;
8-
import gate.Factory;
98
import org.junit.Before;
109
import org.junit.BeforeClass;
1110
import org.junit.Test;
1211

13-
import javax.xml.stream.XMLInputFactory;
14-
import javax.xml.stream.XMLStreamReader;
1512
import java.io.StringReader;
1613
import java.util.HashMap;
1714

@@ -27,7 +24,6 @@ public static void setUpClass() throws Exception {
2724
.execute(App::new);
2825
}
2926

30-
3127
private static App app = null;
3228
private static TestContext context = new TestContext();
3329

@@ -66,14 +62,7 @@ public void testGateXMLToDocument() throws Exception {
6662
final String resultBody = result.getBody();
6763
assertNotNull(resultBody);
6864

69-
Document doc = Factory.newDocument("");
70-
XMLStreamReader reader = XMLInputFactory.newFactory().createXMLStreamReader(
71-
new StringReader(resultBody)
72-
);
73-
do {
74-
reader.next();
75-
} while(reader.getEventType() != XMLStreamReader.START_ELEMENT);
76-
gate.corpora.DocumentStaxUtils.readGateXmlDocument(reader, doc);
65+
Document doc = Utils.xmlToDocument(new StringReader(resultBody));
7766
assertEquals(doc.getContent().toString(), content);
7867
}
7968

@@ -102,4 +91,20 @@ public void testApplicationJsonResponse() throws Exception {
10291
parser.nextToken();
10392
}
10493
}
94+
95+
@Test
96+
public void testCache() {
97+
// Create the Input
98+
input_headers.put("Content-Type", "text/plain");
99+
input.withBody("This is a test. My name is LambdaTestFunction, and I just watched the T.V. show Wanda Vision.");
100+
101+
// Invoke the App
102+
final APIGatewayProxyResponseEvent result = app.handleRequest(input, context);
103+
assertEquals(result.getStatusCode().intValue(), 200);
104+
assertEquals(result.getHeaders().get("x-zae-gate-cache"), "MISS");
105+
106+
final APIGatewayProxyResponseEvent cachedResult = app.handleRequest(input, context);
107+
assertEquals(cachedResult.getStatusCode().intValue(), 200);
108+
assertEquals(cachedResult.getHeaders().get("x-zae-gate-cache"), "HIT");
109+
}
105110
}

0 commit comments

Comments
 (0)