Merge branch 'master' of github.com:ebean-orm/ebean

This commit is contained in:
Rob Bygrave
2026-05-07 22:13:16 +12:00
17 changed files with 20 additions and 607 deletions
@@ -723,19 +723,6 @@ public interface DatabaseBuilder {
@Deprecated
DatabaseBuilder setReadAuditPrepare(ReadAuditPrepare readAuditPrepare);
/**
* Set the configuration for profiling.
*/
default DatabaseBuilder profilingConfig(ProfilingConfig profilingConfig) {
return setProfilingConfig(profilingConfig);
}
/**
* @deprecated migrate to {@link #profilingConfig(ProfilingConfig)}.
*/
@Deprecated
DatabaseBuilder setProfilingConfig(ProfilingConfig profilingConfig);
/**
* Set the suffix appended to the base table to derive the view that contains the union
* of the base table and the history table in order to support asOf queries.
@@ -2490,11 +2477,6 @@ public interface DatabaseBuilder {
*/
TenantCatalogProvider getTenantCatalogProvider();
/**
* Return the configuration for profiling.
*/
ProfilingConfig getProfilingConfig();
/**
* Return the DB schema to use.
*/
@@ -539,8 +539,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
*/
private SlowQueryListener slowQueryListener;
private ProfilingConfig profilingConfig = new ProfilingConfig();
/**
* The mappingLocations for searching xml mapping.
*/
@@ -1028,17 +1026,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
return this;
}
@Override
public ProfilingConfig getProfilingConfig() {
return profilingConfig;
}
@Override
public DatabaseConfig setProfilingConfig(ProfilingConfig profilingConfig) {
this.profilingConfig = profilingConfig;
return this;
}
@Override
public String getDbSchema() {
return dbSchema;
@@ -2137,7 +2124,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
*/
protected void loadSettings(PropertiesWrapper p) {
dbSchema = p.get("dbSchema", dbSchema);
profilingConfig.loadSettings(p, name);
platformConfig.loadSettings(p);
if (platformConfig.isAllQuotedIdentifiers()) {
adjustNamingConventionForAllQuoted();
@@ -1,148 +0,0 @@
package io.ebean.config;
/**
* Configuration for transaction profiling.
*/
public class ProfilingConfig {
/**
* When true transaction profiling is enabled.
*/
private boolean enabled;
/**
* Set true for verbose mode.
*/
private boolean verbose;
/**
* The minimum transaction execution time to be included in profiling.
*/
private long minimumMicros;
/**
* A specific set of profileIds to include in profiling.
*/
private int[] includeProfileIds = {};
/**
* The number of profiles to write per file.
*/
private long profilesPerFile = 1000;
private String directory = "profiling";
/**
* Return true if transaction profiling is enabled.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Set to true to enable transaction profiling.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Return true if verbose mode is used.
*/
public boolean isVerbose() {
return verbose;
}
/**
* Set to true to use verbose mode.
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Return the minimum transaction execution to be included in profiling.
*/
public long getMinimumMicros() {
return minimumMicros;
}
/**
* Set the minimum transaction execution to be included in profiling.
*/
public void setMinimumMicros(long minimumMicros) {
this.minimumMicros = minimumMicros;
}
/**
* Return the specific set of profileIds to include in profiling.
* When not set all transactions with profileIds are included.
*/
public int[] getIncludeProfileIds() {
return includeProfileIds;
}
/**
* Set a specific set of profileIds to include in profiling.
* When not set all transactions with profileIds are included.
*/
public void setIncludeProfileIds(int[] includeProfileIds) {
this.includeProfileIds = includeProfileIds;
}
/**
* Return the number of profiles to write to a single file.
*/
public long getProfilesPerFile() {
return profilesPerFile;
}
/**
* Set the number of profiles to write to a single file.
*/
public void setProfilesPerFile(long profilesPerFile) {
this.profilesPerFile = profilesPerFile;
}
/**
* Return the directory profiling files are put into.
*/
public String getDirectory() {
return directory;
}
/**
* Set the directory profiling files are put into.
*/
public void setDirectory(String directory) {
this.directory = directory;
}
/**
* Load setting from properties.
*/
public void loadSettings(PropertiesWrapper p, String name) {
enabled = p.getBoolean("profiling", enabled);
verbose = p.getBoolean("profiling.verbose", verbose);
directory = p.get("profiling.directory", directory);
profilesPerFile = p.getLong("profiling.profilesPerFile", profilesPerFile);
minimumMicros = p.getLong("profiling.minimumMicros", minimumMicros);
String includeIds = p.get("profiling.includeProfileIds");
if (includeIds != null) {
includeProfileIds = parseIds(includeIds);
}
}
private int[] parseIds(String includeIds) {
String[] ids = includeIds.split(",");
int[] vals = new int[ids.length];
for (int i = 0; i < ids.length; i++) {
vals[i] = Integer.parseInt(ids[i]);
}
return vals;
}
}
@@ -10,18 +10,6 @@ import org.jspecify.annotations.Nullable;
*/
public interface SpiProfileHandler {
/**
* Process the collected transaction profiling information.
* <p>
* Note that profileId and totalMicros are part of the profilingData but passed separately as the handler
* may filter what it processed based on this information (ignore short transactions, only process specific
* profileId transactions etc).
* </p>
*
* @param transactionProfile The transaction profile that has just been collected
*/
void collectTransactionProfile(TransactionProfile transactionProfile);
/**
* Create a profiling stream for this transaction, or return null to not profile this transaction.
* <p>
@@ -405,12 +405,7 @@ public final class InternalConfiguration {
if (handler != null) {
return plugin(handler);
}
ProfilingConfig profilingConfig = config.getProfilingConfig();
if (!profilingConfig.isEnabled()) {
return new NoopProfileHandler();
}
return plugin(new DefaultProfileHandler(profilingConfig));
return new NoopProfileHandler();
}
/**
@@ -169,7 +169,7 @@ public final class OrmQueryProperties implements Serializable {
}
private static String buildImmutableQueryPlanHashSuffix(FetchConfig fetchConfig) {
return fetchConfig == null ? "" : "/c" + fetchConfig.hashCode();
return fetchConfig == null ? "}" : "/c" + fetchConfig.hashCode() + '}';
}
/**
@@ -468,7 +468,6 @@ public final class OrmQueryProperties implements Serializable {
filterMany.queryPlanHash(builder);
}
builder.append(immutableHashSuffix);
builder.append('}');
}
private static void appendSet(StringBuilder builder, Set<String> values) {
@@ -1,271 +0,0 @@
package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import io.ebean.config.ProfilingConfig;
import io.ebean.plugin.Plugin;
import io.ebean.plugin.SpiServer;
import io.ebean.util.IOUtils;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.SpiProfileHandler;
import org.jspecify.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.System.Logger.Level.*;
import static java.time.temporal.ChronoField.*;
/**
* Default profile handler.
* <p>
* Uses ConcurrentLinkedQueue to minimise contention on threads calling collectTransactionProfile().
* <p>
* Uses a sleep backoff on the single threaded consumer that reads the profiles and writes them to files.
*/
public final class DefaultProfileHandler implements SpiProfileHandler, Plugin {
private static final System.Logger log = CoreLog.internal;
private static final DateTimeFormatter DTF;
static {
DTF = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendValue(YEAR, 4)
.appendValue(MONTH_OF_YEAR, 2)
.appendValue(DAY_OF_MONTH, 2)
.appendLiteral('-')
.appendValue(HOUR_OF_DAY, 2)
.appendValue(MINUTE_OF_HOUR, 2)
.appendValue(SECOND_OF_MINUTE, 2)
.appendLiteral('-')
.appendValue(MILLI_OF_SECOND, 3)
.toFormatter();
}
/**
* Low contention choice.
*/
private final Queue<TransactionProfile> queue = new ConcurrentLinkedQueue<>();
private final ExecutorService executor;
private final ReentrantLock lock = new ReentrantLock();
private final File dir;
private final long minMicros;
private final long profilesPerFile;
private final boolean verbose;
private volatile boolean shutdown;
private long profileCounter;
/**
* Slow down polling of transaction profiling queue.
*/
private int sleepBackoff;
private Writer out;
public DefaultProfileHandler(ProfilingConfig config) {
this.verbose = config.isVerbose();
this.minMicros = config.getMinimumMicros();
this.profilesPerFile = config.getProfilesPerFile();
// dedicated single threaded executor for consuming the
// profiling and writing it to file(s)
this.executor = Executors.newSingleThreadExecutor();
this.dir = new File(config.getDirectory());
if (!dir.exists() && !dir.mkdirs()) {
log.log(ERROR, "failed to mkdirs " + dir.getAbsolutePath());
}
incrementFile();
}
/**
* Low contention adding the transaction profile to the queue.
* Minimise the impact to the normal transaction processing (threads).
*/
@Override
public void collectTransactionProfile(TransactionProfile transactionProfile) {
queue.add(transactionProfile);
}
/**
* Create and return a ProfileStream, or null if location is null (implicit transactions
* are not profiled by the default file-based handler).
*/
@Override
public ProfileStream createProfileStream(@Nullable ProfileLocation location, String label) {
if (location == null) {
return null;
}
return new DefaultProfileStream(location, verbose);
}
private void flushCurrentFile() {
lock.lock();
try {
if (out != null) {
try {
out.close();
out = null;
} catch (IOException e) {
log.log(ERROR, "Failed to flush and close transaction profiling file ", e);
}
}
} finally {
lock.unlock();
}
}
/**
* Move to the next file to write to.
*/
private void incrementFile() {
lock.lock();
try {
flushCurrentFile();
try {
String now = DTF.format(LocalDateTime.now());
File file = new File(dir, "txprofile-" + now + ".tprofile");
out = IOUtils.newWriter(file);
} catch (IOException e) {
log.log(ERROR, "Not expected", e);
}
} finally {
lock.unlock();
}
}
/**
* Main loop for polling the queue and processing profiling messages.
*/
private void collect() {
try {
while (!shutdown) {
TransactionProfile profile = queue.poll();
if (profile == null) {
sleep();
} else if (include(profile)) {
write(profile);
}
}
flushCurrentFile();
} catch (Exception e) {
log.log(WARNING, "Error on collect", e);
}
}
/**
* Write the profile to the current file.
*/
private void write(TransactionProfile profile) {
try {
sleepBackoff = 0;
++profileCounter;
StringBuilder sb = new StringBuilder(80);
// header
sb.append(profile.getStartTime()).append(' ')
.append(profile.getLabel()).append(' ')
.append(profile.getTotalMicros()).append(' ');
// summary
appendSummary(profile, sb);
out.write(sb.toString());
if (verbose) {
out.write(' ');
out.write(profile.getData());
}
out.write('\n');
if (profileCounter % profilesPerFile == 0) {
incrementFile();
log.log(DEBUG, "profiled {0} transactions", profileCounter);
}
} catch (IOException e) {
log.log(WARNING, "Error writing transaction profiling", e);
}
}
private void appendSummary(TransactionProfile profile, StringBuilder sb) {
TransactionProfile.Summary summary = profile.getSummary();
sb.append("z:").append(rate(profile.getTotalMicros(), summary.persistCount + summary.queryCount)).append(' ');
sb.append("p:").append(rate(summary.persistMicros, summary.persistBeans)).append(' ');
sb.append("q:").append(rate(summary.queryMicros, summary.queryCount)).append(' ');
sb.append("qm:").append(summary.queryMax).append(' ');
sb.append("qt:").append(summary.queryMicros).append(' ');
sb.append("qc:").append(summary.queryCount).append(' ');
sb.append("qb:").append(summary.queryBeans).append(' ');
sb.append("pt:").append(summary.persistMicros).append(' ');
sb.append("pc:").append(summary.persistCount).append(' ');
sb.append("pb:").append(summary.persistBeans).append(' ');
sb.append("po:").append(summary.persistOneCount).append(' ');
sb.append("pz:").append(rate(summary.persistBeans, summary.persistCount));
}
private int rate(long micros, long count) {
return count < 1 ? 0 : (int) (micros / count);
}
/**
* Return true if the profile should be included (or false for ignored).
*/
private boolean include(TransactionProfile profile) {
return profile.getTotalMicros() >= minMicros;
}
/**
* Sleep backing off towards 250 millis when there is no activity.
* This seems to be simple and decent for our queue consumer.
*/
private void sleep() {
try {
// backoff sleep when nothing is happening
int sleepFor = Math.min(++sleepBackoff, 250);
Thread.sleep(sleepFor);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void configure(SpiServer server) {
// do nothing
}
@Override
public void online(boolean online) {
if (online) {
executor.submit(this::collect);
}
}
@Override
public void shutdown() {
shutdown = true;
log.log(TRACE, "shutting down");
try {
executor.shutdown();
if (!executor.awaitTermination(4, TimeUnit.SECONDS)) {
log.log(INFO, "Shut down timeout exceeded. Terminating profiling consumer thread.");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.log(WARNING, "Interrupt on shutdown", e);
}
flushCurrentFile();
}
}
@@ -1,94 +0,0 @@
package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import org.jspecify.annotations.Nullable;
/**
* Default transaction profiling event collection.
*/
public final class DefaultProfileStream implements ProfileStream {
private final long startNanos;
private final StringBuilder buffer;
private final TransactionProfile profile;
private final TransactionProfile.Summary summary;
DefaultProfileStream(@Nullable ProfileLocation location, boolean verbose) {
this.startNanos = System.nanoTime();
this.profile = new TransactionProfile(System.currentTimeMillis(), location);
this.summary = profile.getSummary();
this.buffer = (verbose) ? new StringBuilder(200) : null;
}
/**
* Return the time offset from the beginning of the transaction.
*/
@Override
public long offset() {
return ((System.nanoTime() - startNanos) / 1_000L);
}
private long exeMicros(long offset) {
return offset() - offset;
}
/**
* Add a query execution event.
*/
@Override
public void addQueryEvent(String event, long offset, String beanName, int beanCount, String queryId, String sql) {
long micros = exeMicros(offset);
summary.addQuery(micros, beanCount);
if (buffer != null) {
add(micros, event, offset, beanName, beanCount, queryId);
}
}
/**
* Add a persist event.
*/
@Override
public void addPersistEvent(String event, long offset, String beanName, int beanCount) {
long micros = exeMicros(offset);
summary.addPersist(micros, beanCount);
if (buffer != null) {
add(micros, event, offset, beanName, beanCount, "");
}
}
/**
* Add the commit/rollback event.
*/
@Override
public void addEvent(String event, long offset) {
long micros = exeMicros(offset);
summary.commitMicros = micros;
if (buffer != null) {
buffer.append(event).append(',');
buffer.append(offset).append(',');
buffer.append(micros).append(';');
}
}
private void add(long micros, String event, long offset, String beanName, int beanCount, String queryId) {
buffer.append(event).append(',');
buffer.append(offset).append(',');
buffer.append(micros).append(',');
buffer.append(beanName).append(',');
buffer.append(beanCount).append(',');
buffer.append(queryId).append(';');
}
/**
* End the transaction profiling.
*/
@Override
public void end(TransactionManager manager, String label) {
profile.setTotalMicros(offset());
if (buffer != null) {
profile.setData(buffer.toString());
}
manager.profileCollect(profile);
}
}
@@ -501,7 +501,7 @@ final class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEve
active = false;
manager.collectMetricReadOnly((System.nanoTime() - startNanos) / 1000L);
if (profileStream != null) {
profileStream.end(manager, null);
profileStream.end(null);
}
}
@@ -856,7 +856,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
}
manager.collectMetric(exeMicros);
if (profileStream != null) {
profileStream.end(manager, label);
profileStream.end(label);
}
}
}
@@ -9,13 +9,9 @@ import org.jspecify.annotations.Nullable;
*/
public final class NoopProfileHandler implements SpiProfileHandler {
@Nullable
@Override
public void collectTransactionProfile(TransactionProfile transactionProfile) {
// do nothing
}
@Override
public ProfileStream createProfileStream(@Nullable ProfileLocation location, String label) {
public ProfileStream createProfileStream(@Nullable ProfileLocation location, @Nullable String label) {
// always return null
return null;
}
@@ -28,5 +28,5 @@ public interface ProfileStream {
/**
* Transaction completed collect the profiling information.
*/
void end(TransactionManager manager, String label);
void end(String label);
}
@@ -428,13 +428,6 @@ public class TransactionManager implements SpiTransactionManager {
cacheNotify.notify(new ServerCacheNotification(touchedTables));
}
/**
* Process the collected transaction profiling information.
*/
final void profileCollect(TransactionProfile transactionProfile) {
profileHandler.collectTransactionProfile(transactionProfile);
}
/**
* Collect execution time for an explicit transaction.
*/
@@ -5,7 +5,6 @@ import io.ebean.plugin.Plugin;
import io.ebean.plugin.SpiServer;
import io.ebeaninternal.api.SpiProfileHandler;
import io.ebeaninternal.server.transaction.ProfileStream;
import io.ebeaninternal.server.transaction.TransactionProfile;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
@@ -39,8 +38,8 @@ public final class OtelProfileHandler implements SpiProfileHandler, Plugin {
@Override
public void configure(SpiServer server) {
if (this.tracer == null) {
this.tracer = GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME);
if (tracer == null) {
tracer = GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME);
}
}
@@ -79,11 +78,4 @@ public final class OtelProfileHandler implements SpiProfileHandler, Plugin {
return new OtelProfileStream(tracer, txnSpan);
}
/**
* The stream handles span lifecycle inline — nothing to do here.
*/
@Override
public void collectTransactionProfile(TransactionProfile transactionProfile) {
// no-op: OtelProfileStream.end() already closed the span
}
}
@@ -84,7 +84,7 @@ final class OtelProfileStream implements ProfileStream {
}
@Override
public void end(TransactionManager manager, String label) {
public void end(String label) {
txnSpan.setAttribute(EBEAN_TOTAL_MICROS, offset());
if (label != null) {
txnSpan.updateName("txn." + label);
@@ -85,7 +85,7 @@ class OtelProfileHandlerTest {
var stream = handler.createProfileStream(null, null);
assertNotNull(stream);
stream.addEvent("c", 0); // commit
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -98,7 +98,7 @@ class OtelProfileHandlerTest {
var stream = handler.createProfileStream(mockLocation("OrderService.placeOrder"), null);
assertNotNull(stream);
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -121,7 +121,7 @@ class OtelProfileHandlerTest {
// First query event should update the transaction span name
stream.addQueryEvent("fm", stream.offset(), "Customer", 5, "qplan-1", "select ...");
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -143,7 +143,7 @@ class OtelProfileHandlerTest {
// Simulate some time passing then record a find_many
stream.addQueryEvent("fm", offset, "Order", 42, "plan-abc", "select ...");
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -166,7 +166,7 @@ class OtelProfileHandlerTest {
stream.addQueryEvent("fo", stream.offset(), "User", 1, "p1", "select from user");
stream.addQueryEvent("fm", stream.offset(), "Order", 10, "p2", "select from order");
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -186,7 +186,7 @@ class OtelProfileHandlerTest {
assertNotNull(stream);
stream.addQueryEvent("fo", stream.offset(), "User", 1, "p1", "");
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -208,7 +208,7 @@ class OtelProfileHandlerTest {
long offset = stream.offset();
stream.addPersistEvent("i", offset, "Order", 1);
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -230,7 +230,7 @@ class OtelProfileHandlerTest {
OtelProfileStream stream = (OtelProfileStream) handler.createProfileStream(mockLocation("Svc.ok"), null);
assertNotNull(stream);
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -246,7 +246,7 @@ class OtelProfileHandlerTest {
OtelProfileStream stream = (OtelProfileStream) handler.createProfileStream(mockLocation("Svc.fail"), null);
assertNotNull(stream);
stream.addEvent("r", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -266,7 +266,7 @@ class OtelProfileHandlerTest {
OtelProfileStream stream = (OtelProfileStream) handler.createProfileStream(mockLocation("Svc.timed"), null);
assertNotNull(stream);
stream.addEvent("c", 0);
stream.end(null, null);
stream.end(null);
} finally {
parent.end();
}
@@ -2,7 +2,6 @@ package org.example.domain;
import io.ebean.Database;
import io.ebean.Transaction;
import io.ebean.config.ProfilingConfig;
import io.ebean.datasource.DataSourceBuilder;
import io.ebean.datasource.DataSourcePool;
import io.ebean.opentelemetry.OtelProfileHandler;
@@ -123,9 +122,6 @@ class OtelJaegerIntegrationTest {
}
private Database createDatabase(Tracer tracer) {
ProfilingConfig profilingConfig = new ProfilingConfig();
profilingConfig.setEnabled(true);
DataSourcePool ds = DataSourceBuilder.create()
.url("jdbc:h2:mem:otelit;DB_CLOSE_DELAY=-1")
.username("sa")
@@ -140,7 +136,6 @@ class OtelJaegerIntegrationTest {
.loadFromProperties()
.ddlGenerate(true)
.ddlRun(true)
.profilingConfig(profilingConfig)
.putServiceObject(SpiProfileHandler.class, new OtelProfileHandler(tracer))
.addClass(OtelOrder.class)
.dataSource(ds)