Skip to content

Commit c097281

Browse files
committed
#839 Protocol 19 and native implementation for inline blobs
1 parent 0b3eae4 commit c097281

File tree

72 files changed

+3299
-262
lines changed

Some content is hidden

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

72 files changed

+3299
-262
lines changed

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,5 @@ SPDX-License-Identifier = "LGPL-2.1-or-later"
105105
[[annotations]]
106106
path = "src/resources/META-INF/services/org.firebirdsql.gds.ng.wire.ProtocolDescriptor"
107107
precedence = "aggregate"
108-
SPDX-FileCopyrightText = "Copyright 2013-2021 Mark Rotteveel"
108+
SPDX-FileCopyrightText = "Copyright 2013-2025 Mark Rotteveel"
109109
SPDX-License-Identifier = "LGPL-2.1-or-later"

devdoc/jdp/jdp-2025-03-implement-protocol-19.adoc

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,25 @@
1515
== Context
1616

1717
Firebird 5.0.3 introduces protocol version 19, which will provide a significant performance improvement for small blobs.
18-
This improvement is achieved by sending small blobs inline during the response to cursor fetch, removing the need to explicitly open and request the blobs one by one.
18+
This improvement is achieved by sending small blobs inline during the response to a cursor fetch, removing the need to explicitly open and request the blobs one by one.
1919

2020
Overview of protocol changes:
2121

2222
* The upper limit for inline blob length is 65535 bytes (0xFFFF);
2323
this includes segment length(s).
2424
* `op_execute`/`op_execute2` has an extra (unsigned) int `p_sqldata_inline_blob_size` with the requested inline blob size sent immediately after `p_sqldata_cursor_flags`
2525
* We don't cover the changes for `op_exec_immediate2` as Jaybird doesn't implement it
26-
* The response to `op_execute2` will have zero or more `op_inline_blob` (114) responses before the `op_sql_response` packet
27-
* The response to `op_fetch` will have zero or more `op_inline_blob` (114) responses before each `op_fetch_response`
26+
* The response to `op_execute2` will have zero or more `op_inline_blob` `(114`) responses before the `op_sql_response` packet
27+
* The response to `op_fetch`/`op_fetch_scroll` will have zero or more `op_inline_blob` (`114`) responses before each `op_fetch_response`
2828
* The `op_line_blob` response is the `P_INLINE_BLOB` message with the following payload:
2929
** `p_tran_id` int (formally short): transaction handle
3030
** `p_blob_id` long: blob id
3131
** `p_blob_info` buffer: blob info with (`isc_info_blob_num_segments`, `isc_info_blob_max_segment`, `isc_info_blob_total_length`, `isc_info_blob_type`, `isc_info_end`)
3232
** `p_blob_data` buffer: blob data (including segment lengths!)
33-
* Two DPB items were added, `isc_dpb_max_blob_cache_size` (159) and `isc_dpb_max_inline_blob_size` (160), which are used to configure the client.
33+
* Two DPB items were added, `isc_dpb_max_blob_cache_size` (`159`) and `isc_dpb_max_inline_blob_size` (`160`), which are used to configure the client.
3434

3535
The JDBC implementation in Jaybird requests blobs with a blob parameter buffer that should match the "`normal`" blob, but API-wise on the `FbDatabase` side, it could be anything.
36+
The fbclient implementation will not use the cached inline blob if any blob parameter buffer is passed.
3637

3738
== Decision
3839

@@ -41,7 +42,7 @@ As Jaybird 5 is the "`long-term support`" version for Java 8, and this is a cons
4142
=== Decision details
4243

4344
* Two connection properties, `maxBlobCacheSize` and `maxInlineBlobSize`, are added and associated with their respective DPB items.
44-
That way the implementation will also support configuring inline blobs for native connections (when using fbclient 5.0.3 or higher).
45+
That way, the implementation will also support configuring inline blobs for native connections (when using fbclient 5.0.3 or higher).
4546
** If `maxInlineBlobSize` is not explicitly set, it will use a default of 64 KiB for pure Java connections;
4647
for native connections, it will use the fbclient default (currently also 64 KiB). +
4748
** Setting `maxInlineBlobSize` to `0` will disable inline blobs
@@ -50,26 +51,19 @@ for native connections, it will use the fbclient default (currently also 10 MiB)
5051
** Setting `maxBlobCacheSize` to `0` will *not* disable inline blobs.
5152
* The received inline blobs will be registered on the `FbWireDatabase` instance in a blob cache, to be returned from `createBlobForInput` as an inline blob instance implementing `FbWireBlob`.
5253
** The blob cache tracks memory use based on the blob length and will hold the blob data *without* segment lengths.
53-
If the blob cache exceeds the `maxInlineBlobSize`, the inline blob will be thrown away.
54-
** Once requested, the blob will be removed from the cache (i.e. a subsequent call to `createBlobForInput` for the same blob id will open a server-side blob).
55-
** If the transaction associated with the blob is committed or rolled back (when `COMMITTED` or `ROLLED_BACK` is notified), all blobs of that transaction are removed from the cache.
56-
57-
=== Pending decisions
58-
59-
The following needs to be investigated in more detail before a final decision can be made.
60-
61-
* The local blob may need to be able to open a server-side blob for info requests, or explicitly throw an exception for those cases.
62-
* We may need some checks on the arguments of the BPB and/or `BlobConfig` to ensure it is suitable to return the local blob, or if a server-side blob needs to be opened, check for a subtype (not available in information items), and possibly the charset id requested.
54+
If the blob cache exceeds the `maxInlineBlobSize`, the inline blob to be registered will be thrown away.
55+
** Once requested, the blob will be removed from the cache (i.e. a subsequent call to `createBlobForInput` for the same transaction and blob id will open a server-side blob).
56+
** If the transaction associated with the blob is committed, rolled back or prepared (when `COMMITTED`, `ROLLED_BACK` or `PREPARED` is notified), all blobs of that transaction are removed from the cache.
57+
* The default value of `maxBlobCacheSize` and `maxInlineBlobSize` can be overridden with system properties `org.firebirdsql.jdbc.defaultMaxBlobCacheSize` and `org.firebirdsql.jdbc.defaultMaxInlineBlobSize`
58+
* The information request on an inline blob (pure Java) will filter the information request (this is similar to the native implementation)
59+
** If a requested information item is unknown (i.e. not in the information items sent in `op_inline_blob`), no exception is thrown, and the filtered information response will not include it.
60+
* On opening a blob that should use the "`default`" settings, Jaybird will no longer provide a parameter buffer or `BlobConfig`.
6361
+
64-
For example, currently Jaybird will set `isc_bpb_source_type` and `isc_bpb_target_type` to the subtype of the field, and for subtype `TEXT` `isc_bpb_target_interp` to the charset id of the field.
65-
We could assume that if source_type and target_type have the same value, we can use the cached inline blob.
66-
* Alternative for previous, there needs to be some mechanism so the caller can signal that the cache *should not* be used.
62+
Rationale: The native implementation will not use the cached inline blob if one is opened with any blob parameter buffer.
6763
+
68-
This is probably too complex and will never actually be used.
69-
* Alternative for previous, there needs to be some mechanism so the caller can signal that the cache *should* be used.
64+
Unfortunately, this does mean undoing changes introduced in Jaybird 5 to always explicitly provide a DPB on open with `isc_bpb_source_type` and `isc_bpb_target_type` set to the known subtype, and -- for `SUB_TYPE TEXT` -- `isc_bpb_target_interp` set to the charset id of the field.
7065
+
71-
This is probably too complex, and would only get used by Jaybird itself.
72-
** A more
66+
We will retain a similar solution creating a BPB for create.
7367

7468
=== Rejected decisions
7569

@@ -82,11 +76,18 @@ It also prevents overloading the meaning of `blobBufferSize`.
8276
* Make `maxBlobCacheSize` set to `0` disable inline blobs as well.
8377
+
8478
Having a single property control enabling and disabling inline blobs is simpler.
79+
* The blob config or blob parameter buffer has an extra option to communicate that the inline blob cache should (or should not) be used.
80+
+
81+
Although this would be a viable solution for pure Java, and we initially implemented a Jaybird-specific BPB-item that signalled the inline cache should be bypassed.
82+
This does not work for the native API, as that will only use an inline blob if no blob parameter buffer is used, and open a remote blob if any blob parameter buffer is provided.
83+
To maintain a coherent API, we decided to do the same for pure Java.
8584

8685
== Consequences
8786

8887
A version 19 protocol implementation will be added.
89-
Inline blobs will be used by default when connecting to Firebird 5.0.3 or higher, but can be disabled by setting `maxInlineBlobSize` to `0`.
88+
Inline blobs will be used by default when connecting to Firebird 5.0.3 or higher, but can be disabled by setting `maxInlineBlobSize` to `0` and/or `maxBlobCacheSize` to `0`.
89+
90+
If only the cache is disabled, the server will still send inline blobs, but the client will immediately discard them.
9091

9192
[appendix]
9293
== License Notice
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1-
// SPDX-FileCopyrightText: Copyright 2015-2023 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2015-2025 Mark Rotteveel
22
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
33
package org.firebirdsql.gds.ng.jna;
44

5+
import org.firebirdsql.gds.impl.GDSServerVersion;
6+
import org.firebirdsql.gds.impl.GDSServerVersionException;
57
import org.firebirdsql.gds.ng.FbAttachment;
68

9+
import java.util.List;
10+
711
/**
812
* @author Mark Rotteveel
913
* @since 3.0
1014
*/
1115
public interface JnaAttachment extends FbAttachment {
16+
17+
/**
18+
* Reports the client library version of this attachment.
19+
* <p>
20+
* The default implementation extracts the last raw version string of {@link #getServerVersion()} and parses that.
21+
* </p>
22+
*
23+
* @return client version, may report {@link GDSServerVersion#INVALID_VERSION} if the implementation can't determine
24+
* the client version or if parsing fails.
25+
* @since 7
26+
*/
27+
default GDSServerVersion getClientVersion() {
28+
GDSServerVersion serverVersion = getServerVersion();
29+
List<String> rawVersions = serverVersion.getRawVersions();
30+
if (rawVersions.isEmpty()) {
31+
return GDSServerVersion.INVALID_VERSION;
32+
}
33+
try {
34+
return GDSServerVersion.parseRawVersion(rawVersions.get(rawVersions.size() - 1));
35+
} catch (GDSServerVersionException e) {
36+
return GDSServerVersion.INVALID_VERSION;
37+
}
38+
}
39+
1240
}

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
@@ -22,6 +22,7 @@
2222
import static org.firebirdsql.gds.ISCConstants.isc_segstr_no_op;
2323
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobGetSegmentNegative;
2424
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobPutSegmentEmpty;
25+
import static org.firebirdsql.jaybird.util.ByteArrayHelper.validateBufferLength;
2526

2627
/**
2728
* Implementation of {@link org.firebirdsql.gds.ng.FbBlob} for native client access.
@@ -111,10 +112,10 @@ public void open() throws SQLException {
111112

112113
final BlobParameterBuffer blobParameterBuffer = getBlobParameterBuffer();
113114
final byte[] bpb;
114-
if (blobParameterBuffer != null) {
115-
bpb = blobParameterBuffer.toBytesWithType();
116-
} else {
115+
if (blobParameterBuffer == null || blobParameterBuffer.isEmpty()) {
117116
bpb = ByteArrayHelper.emptyByteArray();
117+
} else {
118+
bpb = blobParameterBuffer.toBytesWithType();
118119
}
119120
try (var ignored = withLock()) {
120121
checkDatabaseAttached();
@@ -157,6 +158,11 @@ public byte[] getSegment(int sizeRequested) throws SQLException {
157158
checkDatabaseAttached();
158159
checkTransactionActive();
159160
checkBlobOpen();
161+
162+
if (isEof()) {
163+
return ByteArrayHelper.emptyByteArray();
164+
}
165+
160166
ShortByReference actualLength = new ShortByReference();
161167
ByteBuffer responseBuffer = getSegment0(sizeRequested, actualLength);
162168
throwAndClearDeferredException();

src/docs/asciidoc/release_notes.adoc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@ This is especially noticeable in connections with high latency.
433433

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

436+
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.
437+
436438
This optimization is available for Firebird 2.1 and higher, but formally only supported for Firebird 3.0 and higher.
437439

438440
This optimization was backported to Jaybird 5.0.7 and Jaybird 6.0.1.
@@ -456,6 +458,53 @@ This optimization was backported to Jaybird 5.0.7 and Jaybird 6.0.1.
456458

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

461+
[#blob-performance-inline-blob]
462+
==== Inline blob support
463+
464+
Introduced in Firebird 5.0.3 (protocol 19), inline blobs offer a significant performance improvement for querying smallish blobs.
465+
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.
466+
467+
There are two connection properties affecting inline blobs:
468+
469+
`maxInlineBlobSize` (aliases: `max_inline_blob_size`, `isc_dpb_max_inline_blob_size`)::
470+
Maximum size in bytes of the blob (default: `65535`). +
471+
A value of `0` will disable sending of inline blobs.
472+
+
473+
The maximum value is decided by the Firebird server, and is currently `65535`;
474+
this may change in the future
475+
+
476+
If a blob is smaller than the specified size, the server will send it inline.
477+
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.
478+
+
479+
The default can be changed with system property `org.firebirdsql.jdbc.defaultMaxInlineBlobSize`.
480+
481+
`maxBlobCacheSize` (aliases: `max_blob_cache_size`, `isc_dpb max_blob_cache_size`)::
482+
Maximum size in bytes -- per connection -- of the blob cache (default: `10485760` or 10 MiB). +
483+
A value of `0` will disable the cache, but does not disable sending of inline blobs.
484+
Set `maxInlineBlobSize` to `0` to disable sending of inline blobs.
485+
+
486+
For pure Java, only the data size is counted towards the cache size.
487+
For native, the segment lengths also count towards the cache size.
488+
+
489+
The default can be changed with system property `org.firebirdsql.jdbc.defaultMaxBlobCacheSize`.
490+
491+
This feature works with pure Java and native connections when connecting to Firebird 5.0.3 or higher.
492+
For native connections, a Firebird 5.0.3 or higher client library must be used.
493+
494+
If the maximum blob cache size is reached, received inline blobs will be discarded.
495+
For pure Java connections, an inline blob is removed from the cache on first use, or when the transaction associated with the blob ends.
496+
The native client implementation may have different cache eviction rules.
497+
498+
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.
499+
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.
500+
501+
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.
502+
Especially in long-running transactions, this may reduce the effectiveness of this feature.
503+
504+
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.
505+
506+
This optimization was backported to Jaybird 5.0.8 and Jaybird 6.0.2.
507+
459508
// TODO add major changes
460509

461510
[#other-fixes-and-changes]

0 commit comments

Comments
 (0)