Skip to content

Commit 491b2ed

Browse files
authored
Fix CachingHttpContentFactory$CachedHttpContent already released buffer (#12704)
#12681 atomically retain the buffer with a check that it still is in the cache, otherwise delegate to the wrapped content Signed-off-by: Ludovic Orban <[email protected]>
1 parent 4f0b2c6 commit 491b2ed

File tree

2 files changed

+125
-5
lines changed

2 files changed

+125
-5
lines changed

jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/content/CachingHttpContentFactory.java

+24-5
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ public HttpContent getContent(String path) throws IOException
242242
if (!isCacheable(httpContent))
243243
return httpContent;
244244

245-
// The re-mapping function may be run multiple times by compute.
246245
AtomicBoolean added = new AtomicBoolean();
247246
cachingHttpContent = _cache.computeIfAbsent(path, key ->
248247
{
@@ -366,19 +365,39 @@ public String getKey()
366365
@Override
367366
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
368367
{
368+
boolean retained = false;
369369
try
370370
{
371-
_buffer.retain();
372-
sink.write(true, BufferUtil.slice(_buffer.getByteBuffer(), (int)offset, (int)length), Callback.from(_buffer::release, callback));
371+
retained = tryRetain();
372+
if (retained)
373+
sink.write(true, BufferUtil.slice(_buffer.getByteBuffer(), Math.toIntExact(offset), Math.toIntExact(length)), Callback.from(this::release, callback));
374+
else
375+
getWrapped().writeTo(sink, offset, length, callback);
373376
}
374377
catch (Throwable x)
375378
{
376-
// BufferUtil.slice() may fail if offset and/or length are out of bounds.
377-
_buffer.release();
379+
// BufferUtil.slice() may fail if offset and/or length are out of bounds,
380+
// Math.toIntExact may fail too if offset or length are > Integer.MAX_VALUE.
381+
if (retained)
382+
release();
378383
callback.failed(x);
379384
}
380385
}
381386

387+
/**
388+
* Atomically checks that this content still is in cache (so it hasn't been released yet and is still usable) and retain
389+
* its internal buffer if it is.
390+
* @return true if this content can be used and has been retained, false otherwise.
391+
*/
392+
private boolean tryRetain()
393+
{
394+
return _cache.computeIfPresent(_cacheKey, (s, cachingHttpContent) ->
395+
{
396+
_buffer.retain();
397+
return cachingHttpContent;
398+
}) != null;
399+
}
400+
382401
@Override
383402
public void release()
384403
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// ========================================================================
3+
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
4+
//
5+
// This program and the accompanying materials are made available under the
6+
// terms of the Eclipse Public License v. 2.0 which is available at
7+
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
8+
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
9+
//
10+
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
11+
// ========================================================================
12+
//
13+
14+
package org.eclipse.jetty.http.content;
15+
16+
import java.io.ByteArrayOutputStream;
17+
import java.nio.charset.StandardCharsets;
18+
import java.nio.file.Files;
19+
import java.nio.file.Path;
20+
21+
import org.eclipse.jetty.http.MimeTypes;
22+
import org.eclipse.jetty.io.ArrayByteBufferPool;
23+
import org.eclipse.jetty.io.ByteBufferPool;
24+
import org.eclipse.jetty.io.Content;
25+
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
26+
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
27+
import org.eclipse.jetty.util.Blocker;
28+
import org.eclipse.jetty.util.resource.ResourceFactory;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.extension.ExtendWith;
33+
34+
import static org.hamcrest.MatcherAssert.assertThat;
35+
import static org.hamcrest.Matchers.is;
36+
37+
@ExtendWith(WorkDirExtension.class)
38+
public class CachingHttpContentFactoryTest
39+
{
40+
public WorkDir workDir;
41+
private ArrayByteBufferPool.Tracking trackingPool;
42+
private ByteBufferPool.Sized sizedPool;
43+
44+
@BeforeEach
45+
public void setUp()
46+
{
47+
trackingPool = new ArrayByteBufferPool.Tracking();
48+
sizedPool = new ByteBufferPool.Sized(trackingPool);
49+
}
50+
51+
@AfterEach
52+
public void tearDown()
53+
{
54+
assertThat("Leaks: " + trackingPool.dumpLeaks(), trackingPool.getLeaks().size(), is(0));
55+
}
56+
57+
@Test
58+
public void testWriteEvictedContent() throws Exception
59+
{
60+
Path file = Files.writeString(workDir.getEmptyPathDir().resolve("file.txt"), "0123456789abcdefghijABCDEFGHIJ");
61+
ResourceHttpContentFactory resourceHttpContentFactory = new ResourceHttpContentFactory(ResourceFactory.root().newResource(file.getParent()), MimeTypes.DEFAULTS, sizedPool);
62+
CachingHttpContentFactory cachingHttpContentFactory = new CachingHttpContentFactory(resourceHttpContentFactory, sizedPool);
63+
64+
HttpContent content = cachingHttpContentFactory.getContent("file.txt");
65+
66+
// Empty the cache so 'content' gets released.
67+
cachingHttpContentFactory.flushCache();
68+
69+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
70+
try (Blocker.Callback cb = Blocker.callback())
71+
{
72+
content.writeTo(Content.Sink.from(baos), 0L, -1L, cb);
73+
cb.block();
74+
}
75+
assertThat(baos.toString(StandardCharsets.UTF_8), is("0123456789abcdefghijABCDEFGHIJ"));
76+
}
77+
78+
@Test
79+
public void testEvictBetweenWriteToAndSinkWrite() throws Exception
80+
{
81+
Path file = Files.writeString(workDir.getEmptyPathDir().resolve("file.txt"), "0123456789abcdefghijABCDEFGHIJ");
82+
ResourceHttpContentFactory resourceHttpContentFactory = new ResourceHttpContentFactory(ResourceFactory.root().newResource(file.getParent()), MimeTypes.DEFAULTS, sizedPool);
83+
CachingHttpContentFactory cachingHttpContentFactory = new CachingHttpContentFactory(resourceHttpContentFactory, sizedPool);
84+
85+
HttpContent content = cachingHttpContentFactory.getContent("file.txt");
86+
87+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
88+
try (Blocker.Callback cb = Blocker.callback())
89+
{
90+
content.writeTo((last, byteBuffer, callback) ->
91+
{
92+
// Empty the cache so 'content' gets released.
93+
cachingHttpContentFactory.flushCache();
94+
95+
Content.Sink.from(baos).write(last, byteBuffer, callback);
96+
}, 0L, -1L, cb);
97+
cb.block();
98+
}
99+
assertThat(baos.toString(StandardCharsets.UTF_8), is("0123456789abcdefghijABCDEFGHIJ"));
100+
}
101+
}

0 commit comments

Comments
 (0)