Skip to content

Commit 90f6e5b

Browse files
committed
#945 Provide service managers with a way to customize request buffers
1 parent 17544b7 commit 90f6e5b

14 files changed

Lines changed: 278 additions & 39 deletions

devdoc/jdp/jdp-2026-06-service-manager-request-customization.adoc

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
== Status
77

8-
* Draft
9-
* Proposed for: Jaybird 5.0.13, Jaybird 6.0.6, Jaybird 7
8+
* Published: 2026-06-30
9+
* Implemented in: Jaybird 5.0.13, Jaybird 6.0.6, Jaybird 7
1010

1111
== Type
1212

@@ -32,7 +32,7 @@ Exceptions to this are:
3232
* `FBStreamingBackupManager#restoreDatabase`
3333
* Various operations in `FBTraceManager`
3434

35-
(Some might be rewritten to call `executeServicesOperation` instead.)
35+
(Some can be rewritten to call `executeServicesOperation` instead.)
3636

3737
== Decision
3838

@@ -53,16 +53,18 @@ Initially, we'll only add one accessor:
5353
`String operation()`:::
5454
The operation name.
5555

56+
The use of the `ServiceRequestContext` object instead of passing the operation name directly allows for future extension without changing the method signature.
57+
5658
`ServiceManager` receives a property for the `ServiceRequestCustomizer`, and implementations call the customizer before starting the service action.
5759
Implementations that previously did not call `executeServicesOperation` are either rewritten to call it, or are changed to call the customizer themselves.
5860

5961
The operation name is generally the name of the API method initiating the operation.
60-
For example, it reports the method name of the caller of `executeServicesOperation`.
61-
Bar bugs, like deeper callstacks than assumed, the operation names are stable as these are the methods defined in the API of the various service manager interfaces.
62+
For example, it reports the entry point method name into the service manager (i.e. the first method called on the service manager by user code).
63+
For example, calling `BackupManager.backupDatabase()` or `backupDatabase(int)` reports `backupDatabase`, while calling `backupMetadata()` reports operation `backupMetadata` (even though that method calls `backupDatabase(int)`).
6264

63-
Alternatively, a customizer can look at the first argument in the service request buffer (the action), or use solutions like `java.lang.StackWalker` to look at the callstack.
65+
Bar bugs, the operation names are stable as these are the methods defined in the API of the various service manager interfaces.
6466

65-
The use of the `ServiceRequestContext` object instead of passing the operation name directly allows for future extension without changing the method signature.
67+
Alternatively, a customizer can look at the first argument in the service request buffer (the action), or use solutions like `java.lang.StackWalker` to look at the callstack.
6668

6769
For example, the gbak `-INCLUDE_DATA` support added to Jaybird 7 by https://github.com/FirebirdSQL/jaybird/issues/944[#944] can also be realized with:
6870

@@ -78,17 +80,16 @@ backupManager.setServiceRequestCustomizer((request, context) -> {
7880
});
7981
----
8082

81-
(Above example is tentative, and might differ from actual implementation.)
82-
8383
== Consequences
8484

85-
The addition of `ServiceRequestCustomizer` will provide users with an escape hatch for missing features.
85+
The addition of `ServiceRequestCustomizer` provides users with an escape hatch for missing features.
8686
As we consider this a feature of last resort, it is only documented in the API docs, and receives minimal coverage in the release notes.
8787
It will not be documented in the Jaybird manual.
8888
This decision may be revisited in the future.
8989

9090
The customizer gives full control over the service request buffer.
91-
This means that users can change the action performed or make other additions or changes that can can result in errors from the server if wrong information is put in the request buffer.
91+
This means that users can also change the action performed or make other additions or changes.
92+
This can can result in errors from the server if wrong information is put in the request buffer.
9293
Jaybird may also produce errors if the changes result in different service output than expected by the implementation in Jaybird.
9394

9495
Providing this customization feature may result in users implementing their own workarounds, and never reporting an improvement request.

src/main/org/firebirdsql/management/FBServiceManager.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
import static org.firebirdsql.gds.ISCConstants.isc_info_svc_to_eof;
2929
import static org.firebirdsql.gds.ISCConstants.isc_info_truncated;
3030
import static org.firebirdsql.gds.ISCConstants.isc_infunk;
31+
import static org.firebirdsql.gds.VaxEncoding.iscVaxInteger2;
3132
import static org.firebirdsql.jaybird.fb.constants.SpbItems.isc_spb_dbname;
3233
import static org.firebirdsql.jaybird.fb.constants.SpbItems.isc_spb_options;
33-
import static org.firebirdsql.gds.VaxEncoding.iscVaxInteger2;
3434

3535
/**
3636
* An implementation of the basic Firebird Service API functionality.
@@ -44,6 +44,7 @@ public class FBServiceManager implements ServiceManager {
4444
private final FbDatabaseFactory dbFactory;
4545
private @Nullable String database;
4646
private @Nullable OutputStream logger;
47+
private @Nullable ServiceRequestCustomizer serviceRequestCustomizer;
4748

4849
public static final int BUFFER_SIZE = 1024; //1K
4950

@@ -287,6 +288,16 @@ public synchronized void setLogger(@Nullable OutputStream logger) {
287288
this.logger = logger;
288289
}
289290

291+
@Override
292+
public void setServiceRequestCustomizer(@Nullable ServiceRequestCustomizer serviceRequestCustomizer) {
293+
this.serviceRequestCustomizer = serviceRequestCustomizer;
294+
}
295+
296+
@Override
297+
public @Nullable ServiceRequestCustomizer getServiceRequestCustomizer() {
298+
return serviceRequestCustomizer;
299+
}
300+
290301
public FbService attachServiceManager() throws SQLException {
291302
FbService fbService = dbFactory.serviceConnect(serviceProperties);
292303
fbService.attach();
@@ -380,13 +391,57 @@ public void queueService(FbService service) throws SQLException, IOException {
380391
*/
381392
protected final void executeServicesOperation(FbService service, ServiceRequestBuffer srb) throws SQLException {
382393
try {
383-
service.startServiceAction(srb);
394+
service.startServiceAction(customize(srb));
384395
queueService(service);
385396
} catch (IOException ioe) {
386397
throw new SQLException(ioe);
387398
}
388399
}
389400

401+
/**
402+
* Customizes the service request buffer, if a customizer was set.
403+
*
404+
* @param srb
405+
* service request buffer
406+
* @return {@code srb}, possibly modified
407+
* @throws SQLException
408+
* for exceptions thrown by the service request customizer
409+
* @see #setServiceRequestCustomizer(ServiceRequestCustomizer)
410+
* @since 7
411+
*/
412+
protected final ServiceRequestBuffer customize(ServiceRequestBuffer srb) throws SQLException {
413+
ServiceRequestCustomizer customizer = serviceRequestCustomizer;
414+
if (customizer != null) {
415+
var context = new ServiceRequestContext(determineOperation());
416+
try {
417+
customizer.customize(srb, context);
418+
} catch (RuntimeException e) {
419+
throw new SQLException("Service request terminated by ServiceRequestCustomizer", e);
420+
}
421+
}
422+
return srb;
423+
}
424+
425+
/**
426+
* Determines the service operation in this call chain.
427+
*
428+
* @return service operation
429+
*/
430+
private String determineOperation() {
431+
Class<?> serviceManagerClass = getClass();
432+
return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
433+
.walk(frames -> frames
434+
// Skip determineOperation and customize
435+
.skip(2)
436+
// Determine method calls within this service manager
437+
.takeWhile(f -> f.getDeclaringClass().isAssignableFrom(serviceManagerClass)
438+
&& ServiceManager.class.isAssignableFrom(f.getDeclaringClass()))
439+
// Obtain the last stack frame in the stream, which is the initiating operation
440+
.reduce((first, second) -> second)
441+
.map(StackWalker.StackFrame::getMethodName)
442+
.orElse("unknown"));
443+
}
444+
390445
protected ServiceRequestBuffer createRequestBuffer(FbService service, int operation, int options) {
391446
ServiceRequestBuffer srb = service.createServiceRequestBuffer();
392447
srb.addArgument(operation);

src/main/org/firebirdsql/management/FBStreamingBackupManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ protected void addBackupsToRestoreRequestBuffer(FbService service, ServiceReques
178178
private void executeServiceBackupOperation(FbService service, ServiceRequestBuffer srb,
179179
OutputStream backupOutputStream) throws SQLException {
180180
try {
181-
service.startServiceAction(srb);
181+
service.startServiceAction(customize(srb));
182182

183183
ServiceRequestBuffer infoSRB = service.createServiceRequestBuffer();
184184
infoSRB.addArgument(isc_info_svc_to_eof);
@@ -218,7 +218,7 @@ private void executeServiceRestoreOperation(FbService service, ServiceRequestBuf
218218
throw new SQLException("Verbose mode was requested but there is no logger provided.");
219219
}
220220
try {
221-
service.startServiceAction(srb);
221+
service.startServiceAction(customize(srb));
222222

223223
ServiceRequestBuffer infoSRB = service.createServiceRequestBuffer();
224224
infoSRB.addArgument(isc_info_svc_stdin);

src/main/org/firebirdsql/management/FBTraceManager.java

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-FileCopyrightText: Copyright 2009 Thomas Steinmaurer
2-
// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
2+
// SPDX-FileCopyrightText: Copyright 2012-2026 Mark Rotteveel
33
// SPDX-License-Identifier: LGPL-2.1-or-later
44
package org.firebirdsql.management;
55

@@ -168,10 +168,7 @@ public void startTraceSession(@Nullable String traceSessionName, String configur
168168
*/
169169
public void stopTraceSession(int traceSessionId) throws SQLException {
170170
try (FbService service = attachServiceManager()) {
171-
service.startServiceAction(getTraceSPB(service, isc_action_svc_trace_stop, traceSessionId));
172-
queueService(service);
173-
} catch (IOException ioe) {
174-
throw new SQLException(ioe);
171+
executeServicesOperation(service, getTraceSPB(service, isc_action_svc_trace_stop, traceSessionId));
175172
}
176173
}
177174

@@ -183,10 +180,7 @@ public void stopTraceSession(int traceSessionId) throws SQLException {
183180
*/
184181
public void suspendTraceSession(int traceSessionId) throws SQLException {
185182
try (FbService service = attachServiceManager()) {
186-
service.startServiceAction(getTraceSPB(service, isc_action_svc_trace_suspend, traceSessionId));
187-
queueService(service);
188-
} catch (IOException ioe) {
189-
throw new SQLException(ioe);
183+
executeServicesOperation(service, getTraceSPB(service, isc_action_svc_trace_suspend, traceSessionId));
190184
}
191185
}
192186

@@ -198,10 +192,7 @@ public void suspendTraceSession(int traceSessionId) throws SQLException {
198192
*/
199193
public void resumeTraceSession(int traceSessionId) throws SQLException {
200194
try (FbService service = attachServiceManager()) {
201-
service.startServiceAction(getTraceSPB(service, isc_action_svc_trace_resume, traceSessionId));
202-
queueService(service);
203-
} catch (IOException ioe) {
204-
throw new SQLException(ioe);
195+
executeServicesOperation(service, getTraceSPB(service, isc_action_svc_trace_resume, traceSessionId));
205196
}
206197
}
207198

@@ -210,10 +201,7 @@ public void resumeTraceSession(int traceSessionId) throws SQLException {
210201
*/
211202
public void listTraceSessions() throws SQLException {
212203
try (FbService service = attachServiceManager()) {
213-
service.startServiceAction(getTraceSPB(service, isc_action_svc_trace_list));
214-
queueService(service);
215-
} catch (IOException ioe) {
216-
throw new SQLException(ioe);
204+
executeServicesOperation(service, getTraceSPB(service, isc_action_svc_trace_list));
217205
}
218206
}
219207

src/main/org/firebirdsql/management/ServiceManager.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,31 @@ public interface ServiceManager extends ServiceConnectionProperties {
7979
*/
8080
void setLogger(@Nullable OutputStream logger);
8181

82+
/**
83+
* Sets a service request customizer on this service manager. This replaces any previously set customizer.
84+
* <p>
85+
* The customizer is called just before the request is sent to the server, allowing users to modify the request.
86+
* </p>
87+
* <p>
88+
* If you set a customizer to access a feature not implemented by Jaybird, please consider creating an improvement
89+
* ticket on <a href="https://github.com/FirebirdSQL/jaybird/issues">the Jaybird GitHub repository</a> as well.
90+
* </p>
91+
*
92+
* @param customizer
93+
* service request customizer, {@code null} to remove a previous customizer
94+
* @see #getServiceRequestCustomizer()
95+
* @see ServiceRequestCustomizer
96+
* @since 7
97+
*/
98+
void setServiceRequestCustomizer(@Nullable ServiceRequestCustomizer customizer);
99+
100+
/**
101+
* @return service request customizer, {@code null} if none set
102+
* @see #setServiceRequestCustomizer(ServiceRequestCustomizer)
103+
* @since 7
104+
*/
105+
@Nullable ServiceRequestCustomizer getServiceRequestCustomizer();
106+
82107
/**
83108
* Obtains the server version through a service call.
84109
*
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Mark Rotteveel
2+
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
3+
package org.firebirdsql.management;
4+
5+
/**
6+
* Context information about a service request for {@link ServiceRequestCustomizer}.
7+
*
8+
* @param operation
9+
* name of the operation (the initiating service method name)
10+
* @since 7
11+
*/
12+
public record ServiceRequestContext(String operation) {
13+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Mark Rotteveel
2+
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
3+
package org.firebirdsql.management;
4+
5+
import org.firebirdsql.gds.ServiceRequestBuffer;
6+
7+
/**
8+
* Functional interface for modifying service requests.
9+
* <p>
10+
* This can be used to modify service requests before they are sent to the server, for example to access features not
11+
* currently supported by Jaybird.
12+
* </p>
13+
* <p>
14+
* If you implement this interface to access a feature not implemented by Jaybird, please consider creating an
15+
* improvement ticket on <a href="https://github.com/FirebirdSQL/jaybird/issues">the Jaybird GitHub repository</a> as
16+
* well.
17+
* </p>
18+
*
19+
* @see ServiceManager#setServiceRequestCustomizer(ServiceRequestCustomizer)
20+
* @since 7
21+
*/
22+
@FunctionalInterface
23+
public interface ServiceRequestCustomizer {
24+
25+
/**
26+
* Provides access to, and allows customization of, a service request.
27+
* <p>
28+
* Called by the service manager just before the request is sent to the server. Exceptions thrown by the customizer
29+
* terminate the service request before it's sent to the server.
30+
* </p>
31+
* <p>
32+
* Incorrect use (e.g. adding wrong arguments or values, removing or replacing arguments, etc.) may result in
33+
* errors on Firebird or in Jaybird.
34+
* </p>
35+
*
36+
* @param serviceRequest
37+
* service request buffer populated by the service manager, to be modified by this customizer
38+
* @param requestContext
39+
* service request context information
40+
*/
41+
void customize(ServiceRequestBuffer serviceRequest, ServiceRequestContext requestContext);
42+
43+
}

src/test/org/firebirdsql/management/FBBackupManagerTest.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@
3636
import java.sql.DriverManager;
3737
import java.sql.SQLException;
3838
import java.sql.Statement;
39-
import java.util.HashMap;
4039
import java.util.List;
4140
import java.util.Map;
4241
import java.util.function.Function;
4342
import java.util.function.Predicate;
44-
import java.util.stream.Collectors;
4543
import java.util.stream.Stream;
4644

4745
import static java.util.stream.Collectors.toMap;
@@ -71,6 +69,7 @@ class FBBackupManagerTest {
7169
final UsesDatabaseExtension.UsesDatabaseForEach usesDatabase = UsesDatabaseExtension.noDatabase();
7270

7371
private BackupManager backupManager;
72+
private final GetServiceRequestContext getServiceRequestContext = new GetServiceRequestContext();
7473
@TempDir(factory = MappedTempDirFactory.class)
7574
Path tempFolder;
7675

@@ -89,6 +88,7 @@ void setUp() {
8988
backupManager.setParallelWorkers(2);
9089
backupManager.setLogger(System.out);
9190
backupManager.setVerbose(true);
91+
backupManager.setServiceRequestCustomizer(getServiceRequestContext);
9292
}
9393

9494
private void createTestTable() throws SQLException {
@@ -103,19 +103,33 @@ void testBackup() throws Exception {
103103
usesDatabase.createDefaultDatabase();
104104
backupManager.backupDatabase();
105105

106-
MappedPath restorePath = getTempPath("testrestore.fdb");
106+
getServiceRequestContext.assertLastOperation("backupDatabase");
107107

108108
backupManager.clearRestorePaths();
109+
MappedPath restorePath = getTempPath("testrestore.fdb");
109110
String restorePathString = restorePath.toServerPath();
110111
usesDatabase.addDatabase(restorePathString);
111112
backupManager.setDatabase(restorePathString);
112113
backupManager.restoreDatabase();
113114

115+
getServiceRequestContext.assertLastOperation("restoreDatabase");
114116
try (var c = DriverManager.getConnection(getUrl(restorePathString), getDefaultPropertiesForConnection())) {
115117
assertTrue(c.isValid(0));
116118
}
117119
}
118120

121+
@Test
122+
void testBackupMetadata() throws Exception {
123+
usesDatabase.createDefaultDatabase();
124+
backupManager.clearBackupPaths();
125+
MappedPath backupPath = getTempPath("testmetadatabackup.fbk");
126+
backupManager.setBackupPath(backupPath.toServerPath());
127+
backupManager.backupMetadata();
128+
129+
assertTrue(Files.isRegularFile(backupPath.local()), "Expected local file to exist");
130+
getServiceRequestContext.assertLastOperation("backupMetadata");
131+
}
132+
119133
@Test
120134
void testSetBadBufferCount() {
121135
assertThrows(IllegalArgumentException.class, () -> backupManager.setRestorePageBufferCount(-1),
@@ -514,7 +528,6 @@ private static <T extends ObjectReference> Predicate<T> hasSchema(String schemaN
514528
return (T ref) -> ref.size() >= 2 && ref.first().name().equals(schemaName);
515529
}
516530

517-
518531
private MappedPath getTempPath(String name) {
519532
Path localPath = tempFolder.resolve(name);
520533
Path serverPath = transformMappedToDatabasePath(localPath).orElseThrow();

0 commit comments

Comments
 (0)