test(oceanbase): cover LOB expiry and reject malformed chunk payloads

This commit is contained in:
efeire
2026-10-09 19:01:22 +08:00
parent bc872a1815
commit d336ef312d
3 changed files with 33 additions and 0 deletions
@@ -14,6 +14,32 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
class OceanBaseLobValuesTest {
@Test
void idleAndAbsoluteExpiryFreeLocatorsAndRestorePayloadCapacity() throws Exception {
OceanBaseLobValues values = new OceanBaseLobValues((connection, locator, offset, limit) -> chunk("x".repeat(1000), offset, limit), ignored -> 40L * 1024 * 1024);
for (String fieldName : List.of("accessed", "created")) {
Fixture fixture = new Fixture();
var preview = (OceanBaseLobValues.Preview) values.preview(fixture.resultSet, 1, Types.CLOB, "CLOB");
var entriesField = OceanBaseLobValues.class.getDeclaredField("entries");
entriesField.setAccessible(true);
Object entry = ((java.util.Map<?, ?>) entriesField.get(values)).get(preview.ref());
var ageField = entry.getClass().getDeclaredField(fieldName);
ageField.setAccessible(true);
ageField.setLong(entry, System.currentTimeMillis() - (fieldName.equals("accessed") ? 5 : 30) * 60_000L - 1);
assertEquals("expired", values.fetch(fixture.connection, preview.ref(), 0, 1).status());
assertTrue(fixture.freed.get());
assertFalse(values.hasValues());
}
}
@Test
void nclobRetainsTheExistingCompleteReaderWithoutCapturingAPreview() throws Exception {
Fixture fixture = new Fixture();
OceanBaseLobValues values = new OceanBaseLobValues((connection, locator, offset, limit) -> { fail("NCLOB must use its existing complete reader"); return null; });
assertNull(values.preview(fixture.resultSet, 1, Types.NCLOB, "NCLOB"));
assertFalse(values.hasValues());
assertFalse(fixture.freed.get());
}
@Test
void refusesNewLocatorsWhenRetainedPayloadBudgetIsFullAndRestoresCapacityOnRelease() throws Exception {
OceanBaseLobValues values = new OceanBaseLobValues((connection, locator, offset, limit) -> chunk("x".repeat(1000), offset, limit), ignored -> 40L * 1024 * 1024);
@@ -8,6 +8,12 @@ const response = (data: string, next_offset: number, eof = false) => ({ status:
beforeEach(() => mocks.readLargeValueChunk.mockReset());
describe("original-result LOB snapshot consumption", () => {
it.each([{ eof: "false" }, { value_kind: "unknown" }, { data: null }])("rejects malformed payload fields before consuming a chunk: %j", async (invalid) => {
mocks.readLargeValueChunk.mockResolvedValue({ ...response("text", 4, true), ...invalid });
const consume = vi.fn();
await expect(readLargeValueSnapshot(request, () => true, consume)).rejects.toThrow("Invalid LOB chunk payload");
expect(consume).not.toHaveBeenCalled();
});
it("uses server character offsets and preserves Chinese and emoji across chunks", async () => {
mocks.readLargeValueChunk.mockResolvedValueOnce(response("中文😀", 3)).mockResolvedValueOnce(response("末尾😀", 6, true));
expect(await materializeLargeValueSnapshot(request, () => true)).toBe("中文😀末尾😀");
@@ -16,6 +16,7 @@ export async function readLargeValueSnapshot(
const chunk = await api.readLargeValueChunk({ ...request, offset, limit: 4096 });
if (!isCurrent()) throw new Error("LOB result context changed");
if (chunk.status !== "ok") throw new Error(`LOB snapshot ${chunk.status}; execute the query again`);
if (typeof chunk.data !== "string" || typeof chunk.eof !== "boolean" || (chunk.value_kind !== "text" && chunk.value_kind !== "binary")) throw new Error("Invalid LOB chunk payload");
if (!Number.isSafeInteger(chunk.next_offset) || chunk.next_offset < offset || (!chunk.eof && chunk.next_offset === offset)) {
throw new Error("Invalid LOB chunk offset");
}