Skip to content

Commit 6687be1

Browse files
committed
#870 Backport to Jaybird 6: #839 Protocol 19 and native implementation for inline blobs
1 parent 4cee032 commit 6687be1

File tree

71 files changed

+3700
-215
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+3700
-215
lines changed

jaybird-native/src/main/java/org/firebirdsql/gds/ng/jna/JnaAttachment.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,39 @@
2424
*/
2525
package org.firebirdsql.gds.ng.jna;
2626

27+
import org.firebirdsql.gds.impl.GDSServerVersion;
28+
import org.firebirdsql.gds.impl.GDSServerVersionException;
2729
import org.firebirdsql.gds.ng.FbAttachment;
2830

31+
import java.util.List;
32+
2933
/**
3034
* @author Mark Rotteveel
3135
* @since 3.0
3236
*/
3337
public interface JnaAttachment extends FbAttachment {
38+
39+
/**
40+
* Reports the client library version of this attachment.
41+
* <p>
42+
* The default implementation extracts the last raw version string of {@link #getServerVersion()} and parses that.
43+
* </p>
44+
*
45+
* @return client version, may report {@link GDSServerVersion#INVALID_VERSION} if the implementation can't determine
46+
* the client version or if parsing fails.
47+
* @since 6.0.2
48+
*/
49+
default GDSServerVersion getClientVersion() {
50+
GDSServerVersion serverVersion = getServerVersion();
51+
List<String> rawVersions = serverVersion.getRawVersions();
52+
if (rawVersions.isEmpty()) {
53+
return GDSServerVersion.INVALID_VERSION;
54+
}
55+
try {
56+
return GDSServerVersion.parseRawVersion(rawVersions.get(rawVersions.size() - 1));
57+
} catch (GDSServerVersionException e) {
58+
return GDSServerVersion.INVALID_VERSION;
59+
}
60+
}
61+
3462
}

jaybird-native/src/main/java/org/firebirdsql/gds/ng/jna/JnaBlob.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import static org.firebirdsql.gds.ISCConstants.isc_segstr_no_op;
3939
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobGetSegmentNegative;
4040
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobPutSegmentEmpty;
41+
import static org.firebirdsql.jaybird.util.ByteArrayHelper.validateBufferLength;
4142

4243
/**
4344
* Implementation of {@link org.firebirdsql.gds.ng.FbBlob} for native client access.
@@ -127,10 +128,10 @@ public void open() throws SQLException {
127128

128129
final BlobParameterBuffer blobParameterBuffer = getBlobParameterBuffer();
129130
final byte[] bpb;
130-
if (blobParameterBuffer != null) {
131-
bpb = blobParameterBuffer.toBytesWithType();
132-
} else {
131+
if (blobParameterBuffer == null || blobParameterBuffer.isEmpty()) {
133132
bpb = ByteArrayHelper.emptyByteArray();
133+
} else {
134+
bpb = blobParameterBuffer.toBytesWithType();
134135
}
135136
try (var ignored = withLock()) {
136137
checkDatabaseAttached();
@@ -173,6 +174,11 @@ public byte[] getSegment(int sizeRequested) throws SQLException {
173174
checkDatabaseAttached();
174175
checkTransactionActive();
175176
checkBlobOpen();
177+
178+
if (isEof()) {
179+
return ByteArrayHelper.emptyByteArray();
180+
}
181+
176182
ShortByReference actualLength = new ShortByReference();
177183
ByteBuffer responseBuffer = getSegment0(sizeRequested, actualLength);
178184
throwAndClearDeferredException();

src/docs/asciidoc/release_notes.adoc

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ The values were changed from the original default and maximum of 32,767 to:
5454
+
5555
This change improves the performance of preparing statements with a lot of columns and parameters by reducing the number of round-trips needed to retrieve the column and parameter information.
5656
It may also prevent truncation of "`explained plans`" for statements with very complex or extensive plans.
57+
* Improvement: backported inline blob support (Firebird 5.0.3 and higher) from Jaybird 7 (https://github.com/FirebirdSQL/jaybird/issues/870[#870])
58+
+
59+
See also <<blob-performance-inline-blob>>.
5760

5861
=== Jaybird 6.0.1
5962

@@ -833,6 +836,8 @@ This is especially noticeable in connections with high latency.
833836

834837
Artificial testing on local WiFi with small blobs shows around 85% increase in throughput (comparing a 6.0.1-SNAPSHOT against 6.0.0).
835838

839+
The <<blob-performance-inline-blob>> for Firebird 5.0.3 and higher replaces this improvement for smallish blobs, but it still has benefit for blobs larger than `maxInlineBlobSize` or blobs that are discarded when the inline blob cache is full.
840+
836841
This optimization is available for Firebird 2.1 and higher, but formally only supported for Firebird 3.0 and higher.
837842

838843
For native connections, a similar optimization -- but only for reading blobs -- is available when using a Firebird 5.0.2 or higher fbclient, independent of the Jaybird version.
@@ -854,6 +859,53 @@ This optimization is available for Firebird 2.1 and higher, but formally only su
854859

855860
For native connections, a similar optimization is available when using a Firebird 5.0.2 or higher fbclient, independent of the Jaybird version.
856861

862+
[#blob-performance-inline-blob]
863+
==== Inline blob support
864+
865+
Added in: Jaybird 6.0.2, backported from Jaybird 7
866+
867+
Introduced in Firebird 5.0.3 (protocol 19), inline blobs offer a significant performance improvement for querying smallish blobs.
868+
As the name suggests, blobs are sent _inline_ together with the row data, avoiding additional round trips to the server for reading the blob data and blob information.
869+
870+
There are two connection properties affecting inline blobs:
871+
872+
`maxInlineBlobSize` (aliases: `max_inline_blob_size`, `isc_dpb_max_inline_blob_size`)::
873+
Maximum size in bytes of the blob (default: `65535`). +
874+
A value of `0` will disable sending of inline blobs.
875+
+
876+
The maximum value is decided by the Firebird server, and is currently `65535`;
877+
this may change in the future
878+
+
879+
If a blob is smaller than the specified size, the server will send it inline.
880+
The size includes segment lengths, so the actual maximum blob data received is `_N_ * 2` bytes smaller, where _N_ is the number of segments of the actual blob.
881+
+
882+
The default can be changed with system property `org.firebirdsql.jdbc.defaultMaxInlineBlobSize`.
883+
884+
`maxBlobCacheSize` (aliases: `max_blob_cache_size`, `isc_dpb max_blob_cache_size`)::
885+
Maximum size in bytes -- per connection -- of the blob cache (default: `10485760` or 10 MiB). +
886+
A value of `0` will disable the cache, but does not disable sending of inline blobs.
887+
Set `maxInlineBlobSize` to `0` to disable sending of inline blobs.
888+
+
889+
For pure Java, only the data size is counted towards the cache size.
890+
For native, the segment lengths also count towards the cache size.
891+
+
892+
The default can be changed with system property `org.firebirdsql.jdbc.defaultMaxBlobCacheSize`.
893+
894+
This feature works with pure Java and native connections when connecting to Firebird 5.0.3 or higher.
895+
For native connections, a Firebird 5.0.3 or higher client library must be used.
896+
897+
If the maximum blob cache size is reached, received inline blobs will be discarded.
898+
For pure Java connections, an inline blob is removed from the cache on first use, or when the transaction associated with the blob ends.
899+
The native client implementation may have different cache eviction rules.
900+
901+
As pure java connections remove the inline blob from the cache on first use, subsequent attempts to read the same blob -- by getting a different instance of `java.sql.Blob` or through multiple calls to the `ResultSet.getXXX` methods -- will use a server-side blob.
902+
This can also happen if multiple columns or rows, even in different result sets on the same connection, point to the same blob id in the same transaction.
903+
904+
If you execute queries returning blobs, while those blobs are never actually opened, you may fill up the cache and later received inline blobs are then discarded.
905+
Especially in long-running transactions, this may reduce the effectiveness of this feature.
906+
907+
Artificial testing on local WiFi with small blobs (200 bytes) shows a 30,000-45,000% (yes, thousand)footnote:[The wide range of the percentages is due to running the test with a single hop and two hops between client and server, and thus a wide range of latency.] increase in throughput comparing a 6.0.2-SNAPSHOT against 6.0.0, and a 15,000-25,000% increase in throughput comparing a 6.0.2-SNAPSHOT against 6.0.1.
908+
857909
[#blob-performance-min-buf]
858910
==== Minimum `blobBufferSize` 512 bytes
859911

src/jna-test/org/firebirdsql/gds/ng/jna/JnaStatementTest.java

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,33 @@
2020

2121
import org.firebirdsql.common.FBTestProperties;
2222
import org.firebirdsql.common.extension.GdsTypeExtension;
23+
import org.firebirdsql.gds.ClumpletReader;
24+
import org.firebirdsql.gds.impl.GDSServerVersion;
2325
import org.firebirdsql.gds.ng.AbstractStatementTest;
2426
import org.firebirdsql.gds.ng.DatatypeCoder;
27+
import org.firebirdsql.gds.ng.FbBlob;
2528
import org.firebirdsql.gds.ng.FbDatabase;
2629
import org.firebirdsql.gds.ng.fields.RowValue;
2730
import org.firebirdsql.gds.ng.wire.SimpleStatementListener;
31+
import org.firebirdsql.util.FirebirdSupportInfo;
2832
import org.junit.jupiter.api.Test;
2933
import org.junit.jupiter.api.extension.RegisterExtension;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.ValueSource;
3036

37+
import java.nio.charset.StandardCharsets;
3138
import java.sql.SQLException;
3239

40+
import static org.firebirdsql.common.FbAssumptions.assumeFeature;
41+
import static org.firebirdsql.common.matchers.GdsTypeMatchers.isOtherNativeType;
42+
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
43+
import static org.firebirdsql.gds.ISCConstants.fb_info_wire_rcv_bytes;
44+
import static org.firebirdsql.gds.ISCConstants.isc_info_end;
45+
import static org.hamcrest.MatcherAssert.assertThat;
46+
import static org.hamcrest.Matchers.greaterThan;
47+
import static org.hamcrest.Matchers.hasSize;
3348
import static org.junit.jupiter.api.Assertions.*;
49+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
3450

3551
/**
3652
* Tests for JNA statement.
@@ -43,6 +59,11 @@ class JnaStatementTest extends AbstractStatementTest {
4359
@RegisterExtension
4460
static final GdsTypeExtension testType = GdsTypeExtension.supportsNativeOnly();
4561

62+
/**
63+
* See also {@code INLINE_BLOB_TEST_MAX_SIZE} in {@link org.firebirdsql.gds.ng.wire.version19.V19StatementTest}.
64+
*/
65+
protected static final int INLINE_BLOB_TEST_MAX_SIZE = 65531;
66+
4667
private final AbstractNativeDatabaseFactory factory =
4768
(AbstractNativeDatabaseFactory) FBTestProperties.getFbDatabaseFactory();
4869

@@ -234,4 +255,105 @@ public void testSelect_WithParameters_Execute_and_Fetch() throws Exception {
234255
assertNotNull(listener.getSqlCounts(), "Expected SQL counts");
235256
assertEquals(listener.getRows().size(), listener.getSqlCounts().selectCount(), "Unexpected select count");
236257
}
258+
259+
// NOTE The following tests are similar to the tests in V19StatementTest with the same name. In case of the JNA
260+
// tests, they might have been more appropriate in JnaBlobTest, but for consistency with the pure Java tests, we put
261+
// them here.
262+
263+
@ParameterizedTest
264+
@ValueSource(ints = { 0, 1, 500, INLINE_BLOB_TEST_MAX_SIZE })
265+
public void usesInlineBlob_defaultMax(int size) throws Exception {
266+
assumeInlineBlobSupport();
267+
assertFbBlob(size, true);
268+
}
269+
270+
@Test
271+
public void usesNormalBlob_defaultMax() throws Exception {
272+
assumeInlineBlobSupport();
273+
// First size that will produce a "normal" blob
274+
final int size = INLINE_BLOB_TEST_MAX_SIZE + 1;
275+
assertFbBlob(size, false);
276+
}
277+
278+
@Test
279+
public void usesInlineBlob_max16384() throws Exception {
280+
assumeInlineBlobSupport();
281+
replaceDbHandleWithInlineBlobConfig(16384, null);
282+
final int size = 16384 - 2;
283+
assertFbBlob(size, true);
284+
}
285+
286+
@Test
287+
public void usesNormalBlob_max16384() throws Exception {
288+
assumeInlineBlobSupport();
289+
replaceDbHandleWithInlineBlobConfig(16384, null);
290+
final int size = 16384 - 1;
291+
assertFbBlob(size, false);
292+
}
293+
294+
@ParameterizedTest
295+
@ValueSource(ints = { 0, 1 })
296+
public void usesNormalBlob_cacheSizeZero(int size) throws Exception {
297+
assumeInlineBlobSupport();
298+
replaceDbHandleWithInlineBlobConfig(null, 0);
299+
assertFbBlob(size, false);
300+
}
301+
302+
private void assumeInlineBlobSupport() {
303+
assumeThat("Test requires non-embedded type", FBTestProperties.GDS_TYPE, isOtherNativeType());
304+
// Formally, we should also check if we make a TCP/IP connection
305+
assumeFeature(FirebirdSupportInfo::supportsInlineBlobs, "Test requires inline blob support");
306+
GDSServerVersion clientVersion = ((JnaDatabase) db).getClientVersion();
307+
assumeTrue(clientVersion.isEqualOrAbove(5, 0, 3),
308+
"Test requires fbclient version supporting inline blobs, was: " + clientVersion);
309+
}
310+
311+
private void assertFbBlob(int size, boolean expectInlineBlob) throws SQLException {
312+
allocateStatement();
313+
statement.addStatementListener(listener);
314+
statement.prepare(PRODUCE_BLOB);
315+
316+
final DatatypeCoder coder = db.getDatatypeCoder();
317+
var params = RowValue.of(coder.encodeInt(size));
318+
statement.execute(params);
319+
statement.fetchRows(1);
320+
321+
assertThat("expected a row", listener.getRows(), hasSize(1));
322+
RowValue rowValue = listener.getRows().get(0);
323+
long blobId = coder.decodeLong(rowValue.getFieldData(0));
324+
FbBlob blob = db.createBlobForInput(getOrCreateTransaction(), blobId);
325+
326+
assertBlobLengthAndContent(blob, size, expectInlineBlob);
327+
}
328+
329+
private void assertBlobLengthAndContent(FbBlob blob, int size, boolean expectInlineBlob) throws SQLException {
330+
long receivedBytesBefore = getReceivedBytes();
331+
blob.open();
332+
assertEquals(size, blob.length(), "unexpected blob size");
333+
if (size > 0) {
334+
byte[] data = new byte[size];
335+
blob.get(data, 0, size);
336+
assertEquals("x".repeat(size), new String(data, StandardCharsets.US_ASCII));
337+
}
338+
long receivedBytesAfter = getReceivedBytes();
339+
long receivedBytesDifference = receivedBytesAfter - receivedBytesBefore;
340+
if (expectInlineBlob) {
341+
assertEquals(0L, receivedBytesDifference, "expected an inline blob, so no received network data");
342+
} else {
343+
// For server-side blob, more bytes than size will have been received (i.e. open, get segment, etc.)
344+
assertThat("expected a server-side blob", receivedBytesDifference, greaterThan((long) size));
345+
}
346+
}
347+
348+
private long getReceivedBytes() throws SQLException {
349+
return db.getDatabaseInfo(new byte[] { (byte) fb_info_wire_rcv_bytes, isc_info_end }, 20, infoResponse -> {
350+
var reader = new ClumpletReader(ClumpletReader.Kind.InfoResponse, infoResponse);
351+
if (reader.find(fb_info_wire_rcv_bytes)) {
352+
return reader.getLong();
353+
}
354+
throw new IllegalStateException(
355+
"Did not receive item fb_info_wire_rcv_bytes (157), this is probably a 5.0.1 or older fbclient");
356+
});
357+
}
358+
237359
}

src/main/module-info.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@
8484
org.firebirdsql.gds.ng.wire.version13.Version13Descriptor,
8585
org.firebirdsql.gds.ng.wire.version15.Version15Descriptor,
8686
org.firebirdsql.gds.ng.wire.version16.Version16Descriptor,
87-
org.firebirdsql.gds.ng.wire.version18.Version18Descriptor;
87+
org.firebirdsql.gds.ng.wire.version18.Version18Descriptor,
88+
org.firebirdsql.gds.ng.wire.version19.Version19Descriptor;
8889

8990
uses org.firebirdsql.jaybird.props.spi.ConnectionPropertyDefinerSpi;
9091
}

src/main/org/firebirdsql/ds/AbstractConnectionPropertiesDataSource.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,26 @@ public void setAsyncFetch(boolean asyncFetch) {
516516
FirebirdConnectionProperties.super.setAsyncFetch(asyncFetch);
517517
}
518518

519+
@Override
520+
public int getMaxInlineBlobSize() {
521+
return FirebirdConnectionProperties.super.getMaxInlineBlobSize();
522+
}
523+
524+
@Override
525+
public void setMaxInlineBlobSize(int maxInlineBlobSize) {
526+
FirebirdConnectionProperties.super.setMaxInlineBlobSize(maxInlineBlobSize);
527+
}
528+
529+
@Override
530+
public int getMaxBlobCacheSize() {
531+
return FirebirdConnectionProperties.super.getMaxBlobCacheSize();
532+
}
533+
534+
@Override
535+
public void setMaxBlobCacheSize(int maxBlobCacheSize) {
536+
FirebirdConnectionProperties.super.setMaxBlobCacheSize(maxBlobCacheSize);
537+
}
538+
519539
@SuppressWarnings("deprecation")
520540
@Deprecated(since = "5")
521541
@Override

src/main/org/firebirdsql/gds/ISCConstants.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,25 @@ public interface ISCConstants {
602602
int fb_info_username = 147;
603603
int fb_info_sqlrole = 148;
604604

605+
int fb_info_parallel_workers = 149;
606+
607+
// The following items are only known to fbclient 5.0.2 and higher, not to the server.
608+
// The pure Java implementation is not aware of these either.
609+
int fb_info_wire_out_packets = 150;
610+
int fb_info_wire_in_packets = 151;
611+
int fb_info_wire_out_bytes = 152;
612+
int fb_info_wire_in_bytes = 153;
613+
int fb_info_wire_snd_packets = 154;
614+
int fb_info_wire_rcv_packets = 155;
615+
int fb_info_wire_snd_bytes = 156;
616+
int fb_info_wire_rcv_bytes = 157;
617+
int fb_info_wire_roundtrips = 158;
618+
619+
// The following items are only known to fbclient 5.0.3 and higher, not to the server.
620+
// The pure Java implementation is not aware of these either.
621+
int fb_info_max_blob_cache_size = 159;
622+
int fb_info_max_inline_blob_size = 160;
623+
605624
int isc_info_db_impl_rdb_vms = 1;
606625
int isc_info_db_impl_rdb_eln = 2;
607626
int isc_info_db_impl_rdb_eln_dev = 3;

src/main/org/firebirdsql/gds/JaybirdSystemProperties.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public final class JaybirdSystemProperties {
4242
public static final String DEFAULT_ENABLE_PROTOCOL = JDBC_PREFIX + "defaultEnableProtocol";
4343
public static final String DEFAULT_REPORT_SQL_WARNINGS = JDBC_PREFIX + "defaultReportSQLWarnings";
4444
public static final String DEFAULT_ASYNC_FETCH = JDBC_PREFIX + "defaultAsyncFetch";
45+
public static final String DEFAULT_MAX_INLINE_BLOB_SIZE = JDBC_PREFIX + "defaultMaxInlineBlobSize";
46+
public static final String DEFAULT_MAX_BLOB_CACHE_SIZE = JDBC_PREFIX + "defaultMaxBlobCacheSize";
4547
public static final String DATATYPE_CODER_CACHE_SIZE = COMMON_PREFIX + "datatypeCoderCacheSize";
4648
public static final String NATIVE_LIBRARY_SHUTDOWN_DISABLED = COMMON_PREFIX + "nativeResourceShutdownDisabled";
4749
public static final String WIRE_DEFLATE_BUFFER_SIZE = WIRE_PREFIX + "deflateBufferSize";
@@ -117,6 +119,14 @@ public static Boolean getDefaultAsyncFetch() {
117119
return asyncFetch.isBlank() || Boolean.parseBoolean(asyncFetch);
118120
}
119121

122+
public static Integer getDefaultMaxInlineBlobSize() {
123+
return getIntegerSystemPropertyPrivileged(DEFAULT_MAX_INLINE_BLOB_SIZE);
124+
}
125+
126+
public static Integer getDefaultMaxBlobCacheSize() {
127+
return getIntegerSystemPropertyPrivileged(DEFAULT_MAX_BLOB_CACHE_SIZE);
128+
}
129+
120130
private static int getWithDefault(String propertyName, int defaultValue) {
121131
Integer value = getIntegerSystemPropertyPrivileged(propertyName);
122132
return value != null ? value : defaultValue;

0 commit comments

Comments
 (0)