Skip to content

Commit ad0fa2b

Browse files
rma-rripkenMikeNeilson
authored andcommitted
Cleaning up potential resource handling errors. (#1258)
Explicitly calling blob and clob free. Putting streams from blob and clob in try-with-resources blocks. Calling jOOQ fetchStream in try-with-resources. I don't think any of these is actually causing connection leaks but they seem like easy changes for better safety (cherry picked from commit a660ea5)
1 parent ca9167e commit ad0fa2b

14 files changed

Lines changed: 288 additions & 133 deletions

cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939

4040
import javax.servlet.http.HttpServletResponse;
4141
import java.io.InputStream;
42-
import java.util.logging.Logger;
4342

4443
import static com.codahale.metrics.MetricRegistry.name;
4544
import static cwms.cda.api.Controllers.*;
@@ -100,8 +99,9 @@ public void handle(Context ctx) {
10099
} else {
101100
long size = blob.length();
102101
requestResultSize.update(size);
103-
InputStream is = blob.getBinaryStream();
104-
ctx.seekableStream(is, mediaType, size);
102+
try (InputStream is = blob.getBinaryStream()) {
103+
RangeRequestUtil.seekableStream(ctx, is, mediaType, size);
104+
}
105105
}
106106
});
107107
}

cwms-data-api/src/main/java/cwms/cda/api/BlobController.java

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cwms.cda.api;
22

33
import static com.codahale.metrics.MetricRegistry.name;
4+
45
import static cwms.cda.api.Controllers.*;
56

67
import com.codahale.metrics.Histogram;
@@ -28,12 +29,13 @@
2829
import java.io.InputStream;
2930
import java.util.List;
3031
import java.util.Optional;
32+
3133
import javax.servlet.http.HttpServletResponse;
34+
3235
import org.jetbrains.annotations.NotNull;
3336
import org.jooq.DSLContext;
3437

3538

36-
3739
/**
3840
*
3941
*/
@@ -62,32 +64,32 @@ protected DSLContext getDslContext(Context ctx) {
6264
}
6365

6466
@OpenApi(
65-
queryParams = {
66-
@OpenApiParam(name = OFFICE,
67-
description = "Specifies the owning office. If this field is not "
68-
+ "specified, matching information from all offices shall be "
69-
+ "returned."),
70-
@OpenApiParam(name = PAGE,
71-
description = "This end point can return a lot of data, this "
72-
+ "identifies where in the request you are. This is an opaque"
73-
+ " value, and can be obtained from the 'next-page' value in "
74-
+ "the response."),
75-
@OpenApiParam(name = PAGE_SIZE,
76-
type = Integer.class,
77-
description = "How many entries per page returned. Default "
78-
+ DEFAULT_PAGE_SIZE + "."),
79-
@OpenApiParam(name = LIKE,
80-
description = "Posix <a href=\"regexp.html\">regular expression</a> "
81-
+ "describing the blob id's you want")
82-
},
83-
responses = {@OpenApiResponse(status = STATUS_200,
84-
description = "A list of blobs.",
85-
content = {
86-
@OpenApiContent(type = Formats.JSON, from = Blobs.class),
87-
@OpenApiContent(type = Formats.JSONV2, from = Blobs.class),
88-
})
89-
},
90-
tags = {TAG}
67+
queryParams = {
68+
@OpenApiParam(name = OFFICE,
69+
description = "Specifies the owning office. If this field is not "
70+
+ "specified, matching information from all offices shall be "
71+
+ "returned."),
72+
@OpenApiParam(name = PAGE,
73+
description = "This end point can return a lot of data, this "
74+
+ "identifies where in the request you are. This is an opaque"
75+
+ " value, and can be obtained from the 'next-page' value in "
76+
+ "the response."),
77+
@OpenApiParam(name = PAGE_SIZE,
78+
type = Integer.class,
79+
description = "How many entries per page returned. Default "
80+
+ DEFAULT_PAGE_SIZE + "."),
81+
@OpenApiParam(name = LIKE,
82+
description = "Posix <a href=\"regexp.html\">regular expression</a> "
83+
+ "describing the blob id's you want")
84+
},
85+
responses = {@OpenApiResponse(status = STATUS_200,
86+
description = "A list of blobs.",
87+
content = {
88+
@OpenApiContent(type = Formats.JSON, from = Blobs.class),
89+
@OpenApiContent(type = Formats.JSONV2, from = Blobs.class),
90+
})
91+
},
92+
tags = {TAG}
9193
)
9294
@Override
9395
public void getAll(@NotNull Context ctx) {
@@ -130,7 +132,7 @@ public void getAll(@NotNull Context ctx) {
130132
description = "Returns the binary value of the requested blob as a seekable stream with the "
131133
+ "appropriate media type.",
132134
queryParams = {
133-
@OpenApiParam(name = OFFICE, description = "Specifies the owning office."),
135+
@OpenApiParam(name = OFFICE, description = "Specifies the owning office."),
134136
},
135137
tags = {TAG}
136138
)
@@ -151,8 +153,9 @@ public void getOne(@NotNull Context ctx, @NotNull String blobId) {
151153
} else {
152154
long size = blob.length();
153155
requestResultSize.update(size);
154-
InputStream is = blob.getBinaryStream();
155-
ctx.seekableStream(is, mediaType, size);
156+
try (InputStream is = blob.getBinaryStream()) { // is OracleBlobInputStream
157+
RangeRequestUtil.seekableStream(ctx, is, mediaType, size);
158+
}
156159
}
157160
};
158161
if (office.isPresent()) {
@@ -168,12 +171,12 @@ public void getOne(@NotNull Context ctx, @NotNull String blobId) {
168171
description = "Create new Blob",
169172
requestBody = @OpenApiRequestBody(
170173
content = {
171-
@OpenApiContent(from = Blob.class, type = Formats.JSONV2)
174+
@OpenApiContent(from = Blob.class, type = Formats.JSONV2)
172175
},
173176
required = true),
174177
queryParams = {
175-
@OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class,
176-
description = "Create will fail if provided ID already exists. Default: true")
178+
@OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class,
179+
description = "Create will fail if provided ID already exists. Default: true")
177180
},
178181
method = HttpMethod.POST,
179182
tags = {TAG}
@@ -199,8 +202,8 @@ public void create(@NotNull Context ctx) {
199202
},
200203
requestBody = @OpenApiRequestBody(
201204
content = {
202-
@OpenApiContent(from = Blob.class, type = Formats.JSONV2),
203-
@OpenApiContent(from = Blob.class, type = Formats.JSON)
205+
@OpenApiContent(from = Blob.class, type = Formats.JSONV2),
206+
@OpenApiContent(from = Blob.class, type = Formats.JSON)
204207
},
205208
required = true),
206209
method = HttpMethod.PATCH,
@@ -239,10 +242,10 @@ public void update(@NotNull Context ctx, @NotNull String blobId) {
239242
@OpenApi(
240243
description = "Deletes requested blob",
241244
pathParams = {
242-
@OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"),
245+
@OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"),
243246
},
244247
queryParams = {
245-
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the "
248+
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the "
246249
+ "owning office of the blob to be deleted"),
247250
},
248251
method = HttpMethod.DELETE,

cwms-data-api/src/main/java/cwms/cda/api/ClobController.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.jooq.DSLContext;
2727

2828
import javax.servlet.http.HttpServletResponse;
29+
30+
import java.io.InputStream;
2931
import java.util.Objects;
3032
import java.util.Optional;
3133

@@ -175,7 +177,9 @@ public void getOne(@NotNull Context ctx, @NotNull String clobId) {
175177
ctx.status(HttpServletResponse.SC_NOT_FOUND).json(new CdaError("Unable to find "
176178
+ "clob based on given parameters"));
177179
} else {
178-
ctx.seekableStream(c.getAsciiStream(), TEXT_PLAIN, c.length());
180+
try (InputStream is = c.getAsciiStream()) {
181+
RangeRequestUtil.seekableStream(ctx, is, TEXT_PLAIN, c.length());
182+
}
179183
}
180184
});
181185
} else {

cwms-data-api/src/main/java/cwms/cda/api/ForecastFileController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ public void handle(Context ctx) {
109109
} else {
110110
long size = blob.length();
111111
requestResultSize.update(size);
112-
InputStream is = blob.getBinaryStream();
113-
ctx.seekableStream(is, mediaType, size);
112+
try (InputStream is = blob.getBinaryStream()) {
113+
RangeRequestUtil.seekableStream(ctx, is, mediaType, size);
114+
}
114115
}
115116
});
116117
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package cwms.cda.api;
2+
3+
import io.javalin.core.util.Header;
4+
import io.javalin.http.Context;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.io.OutputStream;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
public class RangeRequestUtil {
12+
13+
private RangeRequestUtil() {
14+
// utility class
15+
}
16+
17+
/**
18+
* Javalin has a method very similar to this in its Context class. The issue is that Javalin decided to
19+
* take the InputStream, wrap it in a CompletedFuture and then process the request asynchronously. This
20+
* causes problems when the InputStream is tied to a database connection that gets closed before the
21+
* async processing happens. This method doesn't do the async thing but tries to support the rest.
22+
* @param ctx
23+
* @param is
24+
* @param mediaType
25+
* @param totalBytes
26+
* @throws IOException
27+
*/
28+
public static void seekableStream(Context ctx, InputStream is, String mediaType, long totalBytes) throws IOException {
29+
long from = 0;
30+
long to = totalBytes - 1;
31+
if (ctx.header(Header.RANGE) == null) {
32+
ctx.res.setContentType(mediaType);
33+
// Javalin's version of this method doesn't set the content-length
34+
// Not setting the content-length makes the servlet container use Transfer-Encoding=chunked.
35+
// Chunked is a worse experience overall, seems like we should just set the length if we know it.
36+
writeRange(ctx.res.getOutputStream(), is, from, Math.min(to, totalBytes - 1));
37+
} else {
38+
int chunkSize = 128000;
39+
String rangeHeader = ctx.header(Header.RANGE);
40+
String[] eqSplit = rangeHeader.split("=", 2);
41+
String[] dashSplit = eqSplit[1].split("-", -1); // keep empty trailing part
42+
43+
List<String> requestedRange = Arrays.stream(dashSplit)
44+
.filter(s -> !s.isEmpty())
45+
.collect(java.util.stream.Collectors.toList());
46+
47+
from = Long.parseLong(requestedRange.get(0));
48+
49+
if (from + chunkSize > totalBytes) {
50+
// chunk bigger than file, write all
51+
to = totalBytes - 1;
52+
} else if (requestedRange.size() == 2) {
53+
// chunk smaller than file, to/from specified
54+
to = Long.parseLong(requestedRange.get(1));
55+
} else {
56+
// chunk smaller than file, to/from not specified
57+
to = from + chunkSize - 1;
58+
}
59+
60+
ctx.status(206);
61+
62+
ctx.header(Header.ACCEPT_RANGES, "bytes");
63+
ctx.header(Header.CONTENT_RANGE, "bytes " + from + "-" + to + "/" + totalBytes);
64+
65+
ctx.res.setContentType(mediaType);
66+
ctx.header(Header.CONTENT_LENGTH, String.valueOf(Math.min(to - from + 1, totalBytes)));
67+
writeRange(ctx.res.getOutputStream(), is, from, Math.min(to, totalBytes - 1));
68+
}
69+
}
70+
71+
72+
public static void writeRange(OutputStream out, InputStream in, long from, long to) throws IOException {
73+
writeRange(out, in, from, to, new byte[8192]);
74+
}
75+
76+
public static void writeRange(OutputStream out, InputStream is, long from, long to, byte[] buffer) throws IOException {
77+
long toSkip = from;
78+
while (toSkip > 0) {
79+
long skipped = is.skip(toSkip);
80+
toSkip -= skipped;
81+
}
82+
83+
long bytesLeft = to - from + 1;
84+
while (bytesLeft != 0L) {
85+
int maxRead = (int) Math.min(buffer.length, bytesLeft);
86+
int read = is.read(buffer, 0, maxRead);
87+
if (read == -1) {
88+
break;
89+
}
90+
out.write(buffer, 0, read);
91+
bytesLeft -= read;
92+
}
93+
94+
}
95+
96+
}

cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ public void handle(Context ctx) {
9999
} else {
100100
long size = clob.length();
101101
requestResultSize.update(size);
102-
InputStream is = clob.getAsciiStream();
103-
ctx.seekableStream(is, TEXT_PLAIN, size);
102+
try(InputStream is = clob.getAsciiStream()){
103+
RangeRequestUtil.seekableStream(ctx, is, TEXT_PLAIN, size);
104+
}
104105
}
105106
});
106107
}

0 commit comments

Comments
 (0)