Skip to content

Commit 3bb8872

Browse files
Juan FarrézxBCN Farre_Basurte,Juan_Antonio (IT EDS) EXTERNAL
authored andcommitted
Improve memory management and error handling (#70)
* Improve memory management and error handling Co-authored-by: zxBCN Farre_Basurte,Juan_Antonio (IT EDS) EXTERNAL <juan_antonio.farre_basurte.ext@boehringer-ingelheim.com> (cherry picked from commit def9b0c)
1 parent 41982ca commit 3bb8872

20 files changed

Lines changed: 2525 additions & 401 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Changelog
22

33
## Unreleased
4-
4+
- Improve memory management and error handling ([#70](https://github.com/opendevstack/ods-document-generation-svc/pull/70))
55

66
## [4.0] - 2021-18-11
77

build.gradle

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ buildscript {
6262
}
6363

6464
plugins {
65-
id "com.github.johnrengelman.shadow" version "5.1.0"
65+
id "com.github.johnrengelman.shadow"
6666
id "com.google.osdetector" version "1.6.2"
6767
id "io.spring.dependency-management" version "1.0.8.RELEASE"
6868
}
6969

7070
apply plugin: "application"
71-
apply plugin: "distribution"
71+
apply plugin: "com.github.johnrengelman.shadow"
7272
apply plugin: "groovy"
7373
apply plugin: "jooby"
7474
apply plugin: "jacoco"
@@ -118,28 +118,39 @@ dependencyManagement {
118118
}
119119

120120
dependencies {
121-
compile "com.github.ben-manes.caffeine:caffeine:2.7.0"
122-
compile "com.github.jknack:handlebars:4.1.2"
123-
compile "commons-io:commons-io:2.6"
124-
compile "io.github.openfeign:feign-core:10.2.3"
125-
compile "io.github.openfeign:feign-gson:10.2.3"
126-
compile "io.github.openfeign:feign-okhttp:10.2.3"
127-
compile "net.lingala.zip4j:zip4j:1.3.3"
128-
compile "org.apache.httpcomponents:httpclient:4.5.8"
129-
compile "org.codehaus.groovy:groovy-all:2.5.7"
130-
compile "org.jooby:jooby-jackson"
131-
compile "org.jooby:jooby-netty"
132-
133-
compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
134-
compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}"
135-
136-
testCompile "junit:junit:4.12"
137-
testCompile "com.github.stefanbirkner:system-rules:1.19.0" // for managing environment variables
138-
testCompile "com.github.tomakehurst:wiremock:2.23.2" // for mocking HTTP server reponses
139-
testCompile "io.rest-assured:rest-assured:4.0.0" // for validating REST services
140-
testCompile "org.spockframework:spock-core:1.3-groovy-2.5"
121+
implementation "com.github.ben-manes.caffeine:caffeine:2.7.0"
122+
implementation "com.github.jknack:handlebars:4.1.2"
123+
implementation "commons-io:commons-io:2.11.0"
124+
implementation "io.github.openfeign:feign-core:10.2.3"
125+
implementation "io.github.openfeign:feign-gson:10.2.3"
126+
implementation "io.github.openfeign:feign-okhttp:10.2.3"
127+
implementation "net.lingala.zip4j:zip4j:1.3.3"
128+
implementation "org.apache.httpcomponents:httpclient:4.5.8"
129+
implementation "org.codehaus.groovy:groovy-all:2.5.7"
130+
implementation "org.jooby:jooby-jackson"
131+
implementation "org.jooby:jooby-netty"
132+
133+
implementation "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
134+
implementation "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}"
141135

142136
implementation "org.apache.pdfbox:pdfbox:2.0.24"
137+
138+
testImplementation "junit:junit:4.12"
139+
testImplementation "com.github.stefanbirkner:system-rules:1.19.0" // for managing environment variables
140+
testImplementation "com.github.tomakehurst:wiremock:2.23.2" // for mocking HTTP server reponses
141+
testImplementation "io.rest-assured:rest-assured:4.0.0" // for validating REST services
142+
testImplementation "org.spockframework:spock-core:1.3-groovy-2.5"
143+
144+
testImplementation "cglib:cglib-nodep:3.3.0" // for mocking classes
145+
testImplementation "org.objenesis:objenesis:3.1"
146+
}
147+
148+
import com.github.jengelman.gradle.plugins.shadow.transformers.NewGroovyExtensionModuleTransformer
149+
150+
shadowJar {
151+
mergeServiceFiles()
152+
mergeGroovyExtensionModules()
153+
transform(NewGroovyExtensionModuleTransformer)
143154
}
144155

145156
test {

buildSrc/build.gradle

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
ext {
2+
nexus_url = "${project.findProperty('nexus_url') ?: System.getenv('NEXUS_HOST')}"
3+
nexus_user = "${project.findProperty('nexus_user') ?: System.getenv('NEXUS_USERNAME')}"
4+
nexus_pw = "${project.findProperty('nexus_pw') ?: System.getenv('NEXUS_PASSWORD')}"
5+
no_nexus = (project.findProperty('no_nexus') ?: System.getenv('NO_NEXUS') ?: false).toBoolean()
6+
if (!no_nexus && (nexus_url == "null" || nexus_user == "null" || nexus_pw == "null")) {
7+
throw new GradleException("property no_nexus='false' (or not defined) but at least one of the properties nexus_url, nexus_user or nexus_pw is not configured. Please configure those properties!")
8+
}
9+
10+
def folderRel = (String)("${project.findProperty('nexus_folder_releases') ?: System.getenv('NEXUS_FOLDER_RELEASES')}")
11+
nexusFolderReleases = folderRel == "null" ? "maven-releases" : folderRel
12+
13+
def folderSnaps = (String)("${project.findProperty('nexus_folder_snapshots') ?: System.getenv('NEXUS_FOLDER_SNAPSHOTS')}")
14+
nexusFolderSnapshots = folderSnaps == "null" ? "maven-snapshots" : folderSnaps
15+
16+
snippetsDir = file('build/generated-snippets')
17+
}
18+
19+
repositories {
20+
if (no_nexus) {
21+
println("using repositories 'jcenter' and 'mavenCentral', because property no_nexus=$no_nexus")
22+
jcenter()
23+
mavenCentral()
24+
} else {
25+
println("using nexus repositories")
26+
maven() {
27+
url "${nexus_url}/repository/jcenter/"
28+
credentials {
29+
username = "${nexus_user}"
30+
password = "${nexus_pw}"
31+
}
32+
}
33+
34+
maven() {
35+
url "${nexus_url}/repository/maven-public/"
36+
credentials {
37+
username = "${nexus_user}"
38+
password = "${nexus_pw}"
39+
}
40+
}
41+
42+
maven() {
43+
url "${nexus_url}/repository/atlassian_public/"
44+
credentials {
45+
username = "${nexus_user}"
46+
password = "${nexus_pw}"
47+
}
48+
}
49+
}
50+
}
51+
52+
apply plugin: 'groovy'
53+
54+
dependencies {
55+
implementation gradleApi()
56+
implementation 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
57+
implementation "org.codehaus.plexus:plexus-utils:3.0.24"
58+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package com.github.jengelman.gradle.plugins.shadow.transformers
21+
22+
import shadow.org.apache.tools.zip.ZipEntry
23+
import shadow.org.apache.tools.zip.ZipOutputStream
24+
import org.codehaus.plexus.util.IOUtil
25+
import org.gradle.api.file.FileTreeElement
26+
27+
/**
28+
* Modified from https://github.com/johnrengelman/shadow/blob/7.1.1/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy
29+
* ---
30+
* Modified from eu.appsatori.gradle.fatjar.tasks.PrepareFiles.groovy
31+
* <p>
32+
* Resource transformer that merges Groovy extension module descriptor files into a single file. If there are several
33+
* META-INF/services/org.codehaus.groovy.runtime.ExtensionModule resources spread across many JARs the individual
34+
* entries will all be merged into a single META-INF/services/org.codehaus.groovy.runtime.ExtensionModule resource
35+
* packaged into the resultant JAR produced by the shadowing process.
36+
*/
37+
@CacheableTransformer
38+
class NewGroovyExtensionModuleTransformer implements Transformer {
39+
40+
private static final GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH =
41+
"META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule"
42+
43+
private static final MODULE_NAME_KEY = 'moduleName'
44+
private static final MODULE_VERSION_KEY = 'moduleVersion'
45+
private static final EXTENSION_CLASSES_KEY = 'extensionClasses'
46+
private static final STATIC_EXTENSION_CLASSES_KEY = 'staticExtensionClasses'
47+
48+
private static final MERGED_MODULE_NAME = 'MergedByShadowJar'
49+
private static final MERGED_MODULE_VERSION = '1.0.0'
50+
51+
private final Properties module = new Properties()
52+
53+
@Override
54+
boolean canTransformResource(FileTreeElement element) {
55+
return element.relativePath.pathString == GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH
56+
}
57+
58+
@Override
59+
void transform(TransformerContext context) {
60+
def props = new Properties()
61+
props.load(context.is)
62+
props.each { String key, String value ->
63+
switch (key) {
64+
case MODULE_NAME_KEY:
65+
handle(key, value) {
66+
module.setProperty(key, MERGED_MODULE_NAME)
67+
}
68+
break
69+
case MODULE_VERSION_KEY:
70+
handle(key, value) {
71+
module.setProperty(key, MERGED_MODULE_VERSION)
72+
}
73+
break
74+
case [EXTENSION_CLASSES_KEY, STATIC_EXTENSION_CLASSES_KEY]:
75+
handle(key, value) { String existingValue ->
76+
def newValue = "${existingValue},${value}"
77+
module.setProperty(key, newValue)
78+
}
79+
break
80+
}
81+
}
82+
}
83+
84+
private handle(String key, String value, Closure mergeValue) {
85+
def existingValue = module.getProperty(key)
86+
if (existingValue) {
87+
mergeValue(existingValue)
88+
} else {
89+
module.setProperty(key, value)
90+
}
91+
}
92+
93+
@Override
94+
boolean hasTransformedResource() {
95+
return module.size() > 0
96+
}
97+
98+
@Override
99+
void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) {
100+
ZipEntry entry = new ZipEntry(GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH)
101+
entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time)
102+
os.putNextEntry(entry)
103+
IOUtil.copy(toInputStream(module), os)
104+
os.closeEntry()
105+
}
106+
107+
private static InputStream toInputStream(Properties props) {
108+
def baos = new ByteArrayOutputStream()
109+
props.store(baos, null)
110+
return new ByteArrayInputStream(baos.toByteArray())
111+
}
112+
113+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
moduleName=Resource Handling Extensions
2+
moduleVersion=1.0
3+
extensionClasses=util.Try

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ USER 1001
2323
EXPOSE 8080
2424
ENV JAVA_MEM_XMX="512m" \
2525
JAVA_MEM_XMS="128m" \
26-
JAVA_OPTS="-XX:+UseCompressedOops -XX:+UseG1GC -XX:MaxGCPauseMillis=1000"
26+
JAVA_OPTS="-XX:+UseCompressedOops -XX:+UseG1GC -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=1000"
2727

2828
ENTRYPOINT /app/entrypoint.sh

src/main/groovy/app/App.groovy

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package app
22

33
import com.typesafe.config.ConfigFactory
44
import groovy.util.logging.Slf4j
5-
import org.apache.commons.io.IOUtils
6-
import org.apache.groovy.json.internal.LazyMap
75
import org.jooby.Jooby
86
import org.jooby.MediaType
97
import org.jooby.json.Jackson
8+
import util.DocUtils
9+
import util.FileTools
1010

1111
import java.nio.file.Files
12+
import java.nio.file.StandardOpenOption
1213

1314
import static groovy.json.JsonOutput.prettyPrint
1415
import static groovy.json.JsonOutput.toJson
@@ -19,12 +20,13 @@ import static org.jooby.JoobyExtension.post
1920
class App extends Jooby {
2021

2122
{
23+
2224
use(new Jackson())
2325
use(new DocGen())
2426

2527
post(this, "/document", { req, rsp ->
2628

27-
def body = req.body().to(LazyMap.class)
29+
Map body = req.body().to(HashMap.class)
2830

2931
validateRequestParams(body)
3032

@@ -33,48 +35,56 @@ class App extends Jooby {
3335
log.debug(prettyPrint(toJson(body.data)))
3436
}
3537

36-
def pdf = new DocGen().generate(body.metadata.type, body.metadata.version, body.data)
37-
try{
38-
pdf.withInputStream { is ->
39-
byte[] pdfBytes = IOUtils.toByteArray(is)
40-
rsp.send([
41-
data: Base64.getEncoder().encodeToString(pdfBytes)
42-
])
38+
FileTools.newTempFile('document', '.b64') { dataFile ->
39+
new DocGen().generate(body.metadata.type, body.metadata.version, body.data).withFile { pdf ->
40+
body = null // Not used anymore. Let it be garbage-collected.
41+
dataFile.withOutputStream { os ->
42+
Base64.getEncoder().wrap(os).withStream { encOs ->
43+
Files.copy(pdf, encOs)
44+
}
45+
}
46+
}
47+
def dataLength = Files.size(dataFile)
48+
rsp.length(dataLength + RES_PREFIX.length + RES_SUFFIX.length)
49+
rsp.type(MediaType.json)
50+
def prefixIs = new ByteArrayInputStream(RES_PREFIX)
51+
def suffixIs = new ByteArrayInputStream(RES_SUFFIX)
52+
Files.newInputStream(dataFile, StandardOpenOption.DELETE_ON_CLOSE).initResource { dataIs ->
53+
// Jooby is asynchronous. Upon return of the send method, the response has not necessarily
54+
// been sent. For this reason, we rely on Jooby to close the InputStream and the temporary file
55+
// will be deleted on close.
56+
rsp.send(new SequenceInputStream(Collections.enumeration([prefixIs, dataIs, suffixIs])))
4357
}
44-
}finally{
45-
pdf.delete()
4658
}
4759

4860
})
4961
.consumes(MediaType.json)
5062
.produces(MediaType.json)
5163

5264
get(this, "/health", { req, rsp ->
53-
def documentHtmlFile = Files.createTempFile("document", ".html") << "<html>document</html>"
54-
def headerHtmlFile = Files.createTempFile("header", ".html") << "<html>header</html>"
55-
def footerHtmlFile = Files.createTempFile("footer", ".html") << "<html>footer</html>"
56-
5765
def message = null
5866
def status = "passing"
5967
def statusCode = 200
6068

61-
def data
6269
try {
63-
data = DocGen.Util.convertHtmlToPDF(documentHtmlFile, headerHtmlFile, footerHtmlFile)
70+
FileTools.withTempFile("document", ".html") { documentHtmlFile ->
71+
documentHtmlFile << "<html>document</html>"
72+
73+
DocGen.Util.convertHtmlToPDF(documentHtmlFile, null).withFile { pdf ->
74+
def header = DocUtils.getPDFHeader(pdf)
75+
if (header != PDF_HEADER) {
76+
message = "conversion form HTML to PDF failed"
77+
status = "failing"
78+
statusCode = 500
79+
}
80+
return pdf
81+
}
82+
83+
}
6484
} catch (e) {
6585
message = e.message
6686
status = "failing"
6787
statusCode = 500
68-
} finally {
69-
Files.delete(documentHtmlFile)
70-
Files.delete(headerHtmlFile)
71-
Files.delete(footerHtmlFile)
72-
}
73-
74-
if (!new String(data).startsWith("%PDF-1.4\n")) {
75-
message = "conversion form HTML to PDF failed"
76-
status = "failing"
77-
statusCode = 500
7888
}
7989

8090
def result = [
@@ -92,6 +102,10 @@ class App extends Jooby {
92102
.produces(MediaType.json)
93103
}
94104

105+
private static final RES_PREFIX = '{"data":"'.getBytes('US-ASCII')
106+
private static final RES_SUFFIX = '"}'.getBytes('US-ASCII')
107+
private static final PDF_HEADER = '%PDF-1.4'
108+
95109
private static void validateRequestParams(Map body) {
96110
if (body?.metadata?.type == null) {
97111
throw new IllegalArgumentException("missing argument 'metadata.type'")

0 commit comments

Comments
 (0)