Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.openjproxy.grpc.client;

import com.openjproxy.grpc.OpResult;
import org.openjproxy.jdbc.OjpDriverMetricsHolder;

import java.util.Iterator;
import java.util.NoSuchElementException;

/**
* Iterator wrapper that records Micrometer metrics for a streaming gRPC query result.
*
* <p>For server-streaming RPCs ({@code executeQuery}), the gRPC stub returns an
* {@link Iterator} whose elements arrive lazily over the network. Timing and error
* metrics must therefore be measured across the full lifetime of the iterator, not
* just at the moment the stub call is made.</p>
*
* <p>This wrapper captures the start time at construction and reports the elapsed
* wall-clock time (in milliseconds) to
* {@link OjpDriverMetricsHolder#get()} once the stream is exhausted
* ({@link #hasNext()} returns {@code false}) or a {@link RuntimeException} is thrown
* during iteration. If the iterator is abandoned before exhaustion, the metric will
* not be recorded – a known trade-off documented in the {@code OjpMicrometerDriverMetrics}
* class Javadoc.</p>
*/
class OjpMetricsIterator implements Iterator<OpResult> {

private final Iterator<OpResult> delegate;
private final long startNs;
private boolean completed = false;

OjpMetricsIterator(Iterator<OpResult> delegate) {
this.delegate = delegate;
this.startNs = System.nanoTime();
}

@Override
public boolean hasNext() {
try {
boolean more = delegate.hasNext();
if (!more && !completed) {
completed = true;
OjpDriverMetricsHolder.get().onStatementExecuted(elapsedMs());
}
return more;
} catch (RuntimeException e) {
if (!completed) {
completed = true;
OjpDriverMetricsHolder.get().onStatementFailed();
}
throw e;
}
}

@Override
public OpResult next() {
try {
return delegate.next();
} catch (NoSuchElementException e) {
throw e;
} catch (RuntimeException e) {
if (!completed) {
completed = true;
OjpDriverMetricsHolder.get().onStatementFailed();
}
throw e;
}
}

private long elapsedMs() {
return (System.nanoTime() - startNs) / 1_000_000L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.openjproxy.grpc.GrpcChannelFactory;
import org.openjproxy.jdbc.Connection;
import org.openjproxy.jdbc.LobGrpcIterator;
import org.openjproxy.jdbc.OjpDriverMetricsHolder;

import java.sql.SQLException;
import java.util.Iterator;
Expand Down Expand Up @@ -99,6 +100,7 @@ public OpResult executeUpdate(SessionInfo sessionInfo, String sql, List<Paramete
public OpResult executeUpdate(SessionInfo sessionInfo, String sql, List<Parameter> params, String statementUUID,
Map<String, Object> properties)
throws SQLException {
long startNs = System.nanoTime();
try {
StatementRequest.Builder builder = StatementRequest.newBuilder()
.setSession(sessionInfo)
Expand All @@ -113,8 +115,11 @@ public OpResult executeUpdate(SessionInfo sessionInfo, String sql, List<Paramete
builder.addAllProperties(propertiesToProto(properties));
}

return this.statemetServiceBlockingStub.executeUpdate(builder.build());
OpResult result = this.statemetServiceBlockingStub.executeUpdate(builder.build());
OjpDriverMetricsHolder.get().onStatementExecuted((System.nanoTime() - startNs) / 1_000_000L);
return result;
} catch (StatusRuntimeException e) {
OjpDriverMetricsHolder.get().onStatementFailed();
throw handle(e);
}
}
Expand Down Expand Up @@ -142,8 +147,11 @@ public Iterator<OpResult> executeQuery(SessionInfo sessionInfo, String sql, List
builder.addAllProperties(propertiesToProto(properties));
}

return this.statemetServiceBlockingStub.executeQuery(builder.build());
// Wrap the streaming iterator so that metrics are recorded across the full
// result-stream lifetime, not just at RPC initiation.
return new OjpMetricsIterator(this.statemetServiceBlockingStub.executeQuery(builder.build()));
} catch (StatusRuntimeException e) {
OjpDriverMetricsHolder.get().onStatementFailed();
throw handle(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public void close() throws SQLException {
this.session = null;
}
this.closed = true;
OjpDriverMetricsHolder.get().onConnectionClosed();
}

@Override
Expand Down
2 changes: 2 additions & 0 deletions ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Driver.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ public java.sql.Connection connect(String url, Properties info) throws SQLExcept
sessionInfo.getSessionUUID(), sessionInfo.getConnHash());
} catch (Exception e) {
log.error("Failed to establish connection", e);
OjpDriverMetricsHolder.get().onConnectionFailed();
throw e;
}
OjpDriverMetricsHolder.get().onConnectionCreated();
log.debug("Returning new Connection with sessionInfo: {}", sessionInfo);
return new Connection(sessionInfo, statementService, DatabaseUtils.resolveDbName(cleanUrl));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.openjproxy.jdbc;

/**
* No-op implementation of {@link OjpDriverMetrics} that discards all measurements.
*
* <p>This is the default implementation used when no metrics integration (e.g. Micrometer)
* has been registered via {@link OjpDriverMetricsHolder}. It imposes zero overhead.</p>
*/
public final class NoOpOjpDriverMetrics implements OjpDriverMetrics {

Check warning on line 9 in ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A Singleton implementation was detected. Make sure the use of the Singleton pattern is required and the implementation is the right one for the context.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3srIZdwLXPKpM4Z&open=AZ0FP3srIZdwLXPKpM4Z&pullRequest=394

/** Singleton instance. */
public static final NoOpOjpDriverMetrics INSTANCE = new NoOpOjpDriverMetrics();

private NoOpOjpDriverMetrics() {
}

@Override
public void onConnectionCreated() {

Check failure on line 18 in ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3srIZdwLXPKpM4U&open=AZ0FP3srIZdwLXPKpM4U&pullRequest=394
}

@Override
public void onConnectionFailed() {

Check failure on line 22 in ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3srIZdwLXPKpM4V&open=AZ0FP3srIZdwLXPKpM4V&pullRequest=394
}

@Override
public void onConnectionClosed() {

Check failure on line 26 in ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3srIZdwLXPKpM4W&open=AZ0FP3srIZdwLXPKpM4W&pullRequest=394
}

@Override
public void onStatementExecuted(long durationMs) {

Check failure on line 30 in ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3srIZdwLXPKpM4X&open=AZ0FP3srIZdwLXPKpM4X&pullRequest=394
}

@Override
public void onStatementFailed() {

Check failure on line 34 in ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3srIZdwLXPKpM4Y&open=AZ0FP3srIZdwLXPKpM4Y&pullRequest=394
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.openjproxy.jdbc;

/**
* Interface for collecting driver-side metrics for OJP JDBC connections and statement executions.
*
* <p>This abstraction decouples the JDBC driver from any specific metrics library,
* allowing integrations such as Micrometer (Spring Boot) or no-op (default) to be
* plugged in at runtime via {@link OjpDriverMetricsHolder}.</p>
*
* <p>Implementations must be thread-safe as methods may be called concurrently
* from multiple connections and statement executions.</p>
*/
public interface OjpDriverMetrics {

/**
* Called when a new JDBC connection is successfully established to the OJP server.
*/
void onConnectionCreated();

/**
* Called when a JDBC connection attempt fails.
*/
void onConnectionFailed();

/**
* Called when a JDBC connection is closed.
*/
void onConnectionClosed();

/**
* Called after a SQL statement (query or update) is successfully executed.
*
* @param durationMs the round-trip execution time in milliseconds
*/
void onStatementExecuted(long durationMs);

/**
* Called when a SQL statement execution fails with an exception.
*/
void onStatementFailed();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.openjproxy.jdbc;

/**
* Global holder for the active {@link OjpDriverMetrics} implementation.
*
* <p>The OJP JDBC driver uses this holder as a lightweight service-locator to decouple
* the driver's hot paths from any specific metrics library. By default the holder contains
* {@link NoOpOjpDriverMetrics#INSTANCE}, so the driver incurs no overhead when no metrics
* integration has been registered.</p>
*
* <p>To activate metrics, integrations such as the {@code spring-boot-starter-ojp} Micrometer
* auto-configuration call {@link #set(OjpDriverMetrics)} early in the application lifecycle,
* before the first JDBC connection is created. Example:</p>
*
* <pre>
* OjpDriverMetricsHolder.set(new OjpMicrometerDriverMetrics(meterRegistry));
* </pre>
*
* <p>All methods are thread-safe. The {@code volatile} field ensures that a newly registered
* implementation is immediately visible to all threads.</p>
*/
public final class OjpDriverMetricsHolder {

private static volatile OjpDriverMetrics instance = NoOpOjpDriverMetrics.INSTANCE;

private OjpDriverMetricsHolder() {
}

/**
* Returns the currently active {@link OjpDriverMetrics} implementation.
* Never {@code null}.
*
* @return the active metrics instance
*/
public static OjpDriverMetrics get() {
return instance;
}

/**
* Replaces the active {@link OjpDriverMetrics} implementation.
*
* @param metrics the new implementation; must not be {@code null}
* @throws IllegalArgumentException if {@code metrics} is {@code null}
*/
public static void set(OjpDriverMetrics metrics) {
if (metrics == null) {
throw new IllegalArgumentException("metrics must not be null");
}
instance = metrics;
}

/**
* Resets the holder to the default {@link NoOpOjpDriverMetrics} instance.
* Primarily intended for use in tests.
*/
public static void reset() {
instance = NoOpOjpDriverMetrics.INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.openjproxy.grpc.client;

import com.openjproxy.grpc.OpResult;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.openjproxy.jdbc.OjpDriverMetrics;
import org.openjproxy.jdbc.OjpDriverMetricsHolder;

import java.util.Collections;
import java.util.Iterator;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class OjpMetricsIteratorTest {

private long recordedDuration = -1;
private int failedCount = 0;

private final OjpDriverMetrics capturingMetrics = new OjpDriverMetrics() {
@Override public void onConnectionCreated() {}

Check failure on line 24 in ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3w4IZdwLXPKpM4a&open=AZ0FP3w4IZdwLXPKpM4a&pullRequest=394
@Override public void onConnectionFailed() {}

Check failure on line 25 in ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3w4IZdwLXPKpM4b&open=AZ0FP3w4IZdwLXPKpM4b&pullRequest=394
@Override public void onConnectionClosed() {}

Check failure on line 26 in ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Open-J-Proxy_ojp&issues=AZ0FP3w4IZdwLXPKpM4c&open=AZ0FP3w4IZdwLXPKpM4c&pullRequest=394
@Override public void onStatementExecuted(long durationMs) { recordedDuration = durationMs; }
@Override public void onStatementFailed() { failedCount++; }
};

@AfterEach
void resetHolder() {
OjpDriverMetricsHolder.reset();
}

@Test
void recordsExecutedMetricAfterFullConsumption() {
OjpDriverMetricsHolder.set(capturingMetrics);

Iterator<OpResult> delegate = Collections.<OpResult>emptyList().iterator();
OjpMetricsIterator iterator = new OjpMetricsIterator(delegate);

boolean hasNext = iterator.hasNext();

assertFalse(hasNext);
assertTrue(recordedDuration >= 0, "duration should be >= 0 but was " + recordedDuration);
}

@Test
void recordsExecutedMetricOnlyOnce() {
OjpDriverMetricsHolder.set(capturingMetrics);

Iterator<OpResult> delegate = Collections.<OpResult>emptyList().iterator();
OjpMetricsIterator iterator = new OjpMetricsIterator(delegate);

iterator.hasNext();
recordedDuration = -99;
iterator.hasNext();

// Second hasNext() should not re-record the metric
assertEquals(-99L, recordedDuration);
}

@Test
void recordsFailedMetricWhenHasNextThrows() {
OjpDriverMetricsHolder.set(capturingMetrics);

Iterator<OpResult> failingDelegate = new Iterator<>() {
@Override public boolean hasNext() { throw new RuntimeException("stream error"); }
@Override public OpResult next() { return null; }
};
OjpMetricsIterator iterator = new OjpMetricsIterator(failingDelegate);

assertThrows(RuntimeException.class, iterator::hasNext);
assertEquals(1, failedCount);
}

@Test
void recordsFailedMetricWhenNextThrows() {
OjpDriverMetricsHolder.set(capturingMetrics);

Iterator<OpResult> failingDelegate = new Iterator<>() {
@Override public boolean hasNext() { return true; }
@Override public OpResult next() { throw new RuntimeException("stream error"); }
};
OjpMetricsIterator iterator = new OjpMetricsIterator(failingDelegate);

assertThrows(RuntimeException.class, iterator::next);
assertEquals(1, failedCount);
}

@Test
void recordsFailedMetricOnlyOnce() {
OjpDriverMetricsHolder.set(capturingMetrics);

Iterator<OpResult> failingDelegate = new Iterator<>() {
@Override public boolean hasNext() { throw new RuntimeException("stream error"); }
@Override public OpResult next() { return null; }
};
OjpMetricsIterator iterator = new OjpMetricsIterator(failingDelegate);

assertThrows(RuntimeException.class, iterator::hasNext);
assertThrows(RuntimeException.class, iterator::hasNext);

assertEquals(1, failedCount);
}

@Test
void delegatesHasNextAndNext() {
OjpDriverMetricsHolder.set(capturingMetrics);

OpResult result = OpResult.getDefaultInstance();
Iterator<OpResult> delegate = Collections.singletonList(result).iterator();
OjpMetricsIterator iterator = new OjpMetricsIterator(delegate);

assertTrue(iterator.hasNext());
assertSame(result, iterator.next());
assertFalse(iterator.hasNext());
}
}
Loading