Skip to content
Merged
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
65 changes: 40 additions & 25 deletions storm-core/src/main/java/st/orm/core/spi/WeakInterner.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,21 @@
import java.util.Map;
import java.util.WeakHashMap;
import st.orm.Entity;
import st.orm.Ref;

/**
* A weak interner that ensures canonical instances of objects while holding them weakly to permit garbage collection.
*
* <p>This class uses a dual-path interning strategy optimized for different object types:</p>
* <ul>
* <li><b>Entities</b>: Uses primary key-based lookup via {@link Ref} for efficient equality checks. Entities are
* stored in a separate map with {@link ReferenceQueue}-based cleanup to ensure stale entries are removed when
* entities are garbage collected.</li>
* <li><b>Entities</b>: Keyed by concrete type and primary key for efficient lookup. Entities are stored with
* {@link ReferenceQueue}-based cleanup to ensure stale entries are removed when entities are garbage
* collected.</li>
* <li><b>Non-entities</b>: Uses object equality-based lookup via {@link WeakHashMap}, which provides automatic
* cleanup when objects are no longer strongly referenced.</li>
* </ul>
*
* <p>The primary key-based lookup for entities avoids potentially expensive deep equality checks on complex entity
* objects, while maintaining correct identity semantics (same primary key = same canonical instance).</p>
* objects, while maintaining correct identity semantics (same type and primary key = same canonical instance).</p>
*
* <p>This class is not thread-safe. A new instance is expected to be created for each result set processing call,
* ensuring that interning is scoped to a single query execution.</p>
Expand All @@ -52,8 +51,11 @@ public final class WeakInterner {
/** Queue for tracking garbage-collected entities to enable lazy cleanup of {@link #entityMap}. */
private ReferenceQueue<Entity<?>> queue;

/** Map for entities, using {@link Ref} (primary key) for efficient lookup. Keys are held strongly. */
private Map<Ref<?>, RefWeakReference> entityMap;
/**
* Map for entities, keyed by concrete type then primary key. The two-level structure lets the hot path look up
* and store entities using the primary key value already in hand.
*/
private Map<Class<?>, Map<Object, PkWeakReference>> entityMap;

/**
* Creates a new weak interner.
Expand All @@ -68,8 +70,8 @@ public WeakInterner() {
* Interns the given object, ensuring that only one canonical instance exists. If an equivalent object is already
* present, returns the existing instance. Otherwise, adds the new object to the interner and returns it.
*
* <p>For {@link Entity} instances, lookup is performed using the entity's primary key (via {@link Ref}) for
* efficiency. For all other objects, lookup is based on object equality.</p>
* <p>For {@link Entity} instances, lookup is performed using the entity's type and primary key for efficiency.
* For all other objects, lookup is based on object equality.</p>
*
* @param object the object to intern.
* @param <T> the type of the object.
Expand Down Expand Up @@ -101,8 +103,11 @@ public <E extends Entity<?>> E get(@Nonnull Class<E> entityType, @Nonnull Object
return null;
}
drainQueue();
Ref<?> ref = Ref.of(entityType, pk);
WeakReference<Entity<?>> existing = entityMap.get(ref);
Map<Object, PkWeakReference> byPk = entityMap.get(entityType);
if (byPk == null) {
return null;
}
PkWeakReference existing = byPk.get(pk);
if (existing != null) {
Entity<?> result = existing.get();
if (result != null) {
Expand All @@ -114,7 +119,7 @@ public <E extends Entity<?>> E get(@Nonnull Class<E> entityType, @Nonnull Object
}

/**
* Interns an entity using its primary key (via {@link Ref}) for efficient lookup.
* Interns an entity using its type and primary key for efficient lookup.
*
* <p>This avoids expensive deep equality checks on complex entity objects. The entity is stored with a weak
* reference, and cleanup is handled via {@link #drainQueue()} when entities are garbage collected.</p>
Expand All @@ -130,16 +135,18 @@ private <E extends Entity<?>> E internEntity(@Nonnull E entity) {
} else {
drainQueue();
}
Ref<?> ref = Ref.of(entity);
WeakReference<Entity<?>> existing = entityMap.get(ref);
Class<?> type = entity.getClass();
Object pk = entity.id();
Map<Object, PkWeakReference> byPk = entityMap.computeIfAbsent(type, k -> new HashMap<>());
PkWeakReference existing = byPk.get(pk);
if (existing != null) {
var result = existing.get();
if (result != null) {
//noinspection unchecked
return (E) result;
}
}
entityMap.put(ref, new RefWeakReference(ref, entity, queue));
byPk.put(pk, new PkWeakReference(type, pk, entity, queue));
return entity;
}

Expand Down Expand Up @@ -177,29 +184,37 @@ private <T> T internObject(@Nonnull T object) {
/**
* Removes stale entries from {@link #entityMap} by polling the reference queue.
*
* <p>When an entity is garbage collected, its {@link RefWeakReference} is enqueued. This method polls the queue
* <p>When an entity is garbage collected, its {@link PkWeakReference} is enqueued. This method polls the queue
* and removes the corresponding entries from the map. Uses a two-argument remove to ensure only the exact
* weak reference is removed, preventing removal of a newer entry with the same key.</p>
*/
private void drainQueue() {
RefWeakReference weakReference;
while ((weakReference = (RefWeakReference) queue.poll()) != null) {
entityMap.remove(weakReference.ref, weakReference);
PkWeakReference weakReference;
while ((weakReference = (PkWeakReference) queue.poll()) != null) {
Map<Object, PkWeakReference> byPk = entityMap.get(weakReference.type);
if (byPk != null) {
// Two-argument remove ensures only the exact stale reference is removed, never a newer entry that
// reused the same primary key.
byPk.remove(weakReference.pk, weakReference);
}
}
}

/**
* A weak reference to an entity that retains the associated {@link Ref} for map cleanup.
* A weak reference to an entity that retains its type and primary key for map cleanup.
*
* <p>When the entity is garbage collected, this reference is enqueued in the {@link ReferenceQueue}, allowing
* {@link #drainQueue()} to remove the corresponding entry from {@link #entityMap} using the stored ref.</p>
* {@link #drainQueue()} to remove the corresponding entry from {@link #entityMap} using the stored type and
* primary key.</p>
*/
private static final class RefWeakReference extends WeakReference<Entity<?>> {
final Ref<?> ref;
private static final class PkWeakReference extends WeakReference<Entity<?>> {
final Class<?> type;
final Object pk;

RefWeakReference(Ref<?> ref, Entity<?> referent, ReferenceQueue<? super Entity<?>> q) {
PkWeakReference(Class<?> type, Object pk, Entity<?> referent, ReferenceQueue<? super Entity<?>> q) {
super(referent, q);
this.ref = ref;
this.type = type;
this.pk = pk;
}
}
}
183 changes: 115 additions & 68 deletions storm-core/src/main/java/st/orm/core/template/impl/QueryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,12 @@ protected <T> Spliterator<T> rowSpliterator(@Nonnull ResultSet resultSet, int co
}
var columnSkipper = mapper.columnSkipper();
var calendarSupplier = lazy(() -> Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC)));
// Resolve the per-column reader once per query rather than re-evaluating the target-type dispatch for every
// column of every row. The column types are fixed for the lifetime of the result set.
ColumnReader[] readers = new ColumnReader[columnCount];
for (int i = 0; i < columnCount; i++) {
readers[i] = columnReaderFor(types[i]);
}
return new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED) {
@Override
public boolean tryAdvance(@Nonnull Consumer<? super T> action) {
Expand All @@ -636,10 +642,10 @@ public boolean tryAdvance(@Nonnull Consumer<? super T> action) {
if (columnSkipper != null) {
// Skip decoding non-key columns of entities that are already cached.
columnSkipper.readRow(args, index ->
readColumnValue(resultSet, index + 1, types[index], calendarSupplier));
readers[index].read(resultSet, index + 1, calendarSupplier));
} else {
for (int i = 0; i < columnCount; i++) {
args[i] = readColumnValue(resultSet, i + 1, types[i], calendarSupplier);
args[i] = readers[i].read(resultSet, i + 1, calendarSupplier);
}
}
action.accept(mapper.newInstance(args));
Expand All @@ -653,75 +659,116 @@ public boolean tryAdvance(@Nonnull Consumer<? super T> action) {
};
}

private static Object readColumnValue(
@Nonnull ResultSet rs,
int columnIndex,
@Nonnull Class<?> targetType,
@Nonnull Supplier<Calendar> calendarSupplier
) throws SQLException {
Object value = switch (targetType) {
// Primitives & basics.
case Class<?> c when c == Short.TYPE || c == Short.class -> rs.getShort(columnIndex);
case Class<?> c when c == Integer.TYPE || c == Integer.class -> rs.getInt(columnIndex);
case Class<?> c when c == Long.TYPE || c == Long.class -> rs.getLong(columnIndex);
case Class<?> c when c == Float.TYPE || c == Float.class -> rs.getFloat(columnIndex);
case Class<?> c when c == Double.TYPE || c == Double.class -> rs.getDouble(columnIndex);
case Class<?> c when c == Byte.TYPE || c == Byte.class -> rs.getByte(columnIndex);
case Class<?> c when c == Boolean.TYPE || c == Boolean.class -> rs.getBoolean(columnIndex);
case Class<?> c when c == String.class -> rs.getString(columnIndex);
case Class<?> c when c == BigDecimal.class -> rs.getBigDecimal(columnIndex);
case Class<?> c when c == ByteBuffer.class -> {
byte[] bytes = rs.getBytes(columnIndex);
yield bytes != null ? ByteBuffer.wrap(bytes).asReadOnlyBuffer() : null;
}
case Class<?> c when c == UUID.class -> {
Object obj = rs.getObject(columnIndex);
yield obj instanceof UUID u ? u : obj != null ? UUID.fromString(obj.toString()) : null;
}
case Class<?> c when c.isEnum() -> rs.getString(columnIndex); // Enum handled by mapper.
case Class<?> c when c == java.util.Date.class -> {
Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
yield ts != null ? new java.util.Date(ts.getTime()) : null;
}
case Class<?> c when c == Calendar.class -> {
Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
if (ts == null) yield null;
Calendar out = (Calendar) calendarSupplier.get().clone();
/**
* Reads a single column value from a result set, decoding it to the resolved target type. One reader is resolved
* per column (see {@link #columnReaderFor}) and reused for every row, so the target-type dispatch is not repeated
* for every column of every row.
*/
@FunctionalInterface
private interface ColumnReader {
Object read(@Nonnull ResultSet rs, int columnIndex, @Nonnull Supplier<Calendar> calendarSupplier)
throws SQLException;
}

/**
* Cache of resolved column readers, keyed by target type. Readers are stateless (the calendar is supplied per
* call), so a single instance per type is shared across every query for the lifetime of the VM. The value is
* computed at most once per type and read on a fast, lock-free path.
*/
private static final ClassValue<ColumnReader> COLUMN_READERS = new ClassValue<>() {
@Override
protected ColumnReader computeValue(@Nonnull Class<?> type) {
return buildColumnReader(type);
}
};

/**
* Resolves the column reader for the given target type, computing it at most once per type for the lifetime of
* the VM.
*/
private static ColumnReader columnReaderFor(@Nonnull Class<?> targetType) {
return COLUMN_READERS.get(targetType);
}

/**
* Builds the column reader for the given target type. Primitive-returning JDBC getters honour
* {@link ResultSet#wasNull()} so that a SQL NULL maps to {@code null} rather than the getter's zero value;
* reference-returning getters already yield {@code null} for SQL NULL, so no {@code wasNull} probe is needed.
*/
private static ColumnReader buildColumnReader(@Nonnull Class<?> targetType) {
return switch (targetType) {
// Ordered most-common-first. Resolution is paid once per column per query, so this mainly trims the
// setup path, but keeping the frequent types (identifiers, names, flags) at the front is free.
case Class<?> c when c == String.class -> (rs, i, cal) -> rs.getString(i);
case Class<?> c when c == Integer.TYPE || c == Integer.class ->
(rs, i, cal) -> { int v = rs.getInt(i); return rs.wasNull() ? null : v; };
case Class<?> c when c == Long.TYPE || c == Long.class ->
(rs, i, cal) -> { long v = rs.getLong(i); return rs.wasNull() ? null : v; };
case Class<?> c when c == Boolean.TYPE || c == Boolean.class ->
(rs, i, cal) -> { boolean v = rs.getBoolean(i); return rs.wasNull() ? null : v; };
case Class<?> c when c == Double.TYPE || c == Double.class ->
(rs, i, cal) -> { double v = rs.getDouble(i); return rs.wasNull() ? null : v; };
// Remaining numeric primitives. wasNull distinguishes a genuine zero from a SQL NULL.
case Class<?> c when c == Short.TYPE || c == Short.class ->
(rs, i, cal) -> { short v = rs.getShort(i); return rs.wasNull() ? null : v; };
case Class<?> c when c == Float.TYPE || c == Float.class ->
(rs, i, cal) -> { float v = rs.getFloat(i); return rs.wasNull() ? null : v; };
case Class<?> c when c == Byte.TYPE || c == Byte.class ->
(rs, i, cal) -> { byte v = rs.getByte(i); return rs.wasNull() ? null : v; };
case Class<?> c when c == BigDecimal.class -> (rs, i, cal) -> rs.getBigDecimal(i);
case Class<?> c when c == ByteBuffer.class -> (rs, i, cal) -> {
byte[] bytes = rs.getBytes(i);
return bytes != null ? ByteBuffer.wrap(bytes).asReadOnlyBuffer() : null;
};
case Class<?> c when c == UUID.class -> (rs, i, cal) -> {
Object obj = rs.getObject(i);
return obj instanceof UUID u ? u : obj != null ? UUID.fromString(obj.toString()) : null;
};
case Class<?> c when c.isEnum() -> (rs, i, cal) -> rs.getString(i); // Enum handled by mapper.
case Class<?> c when c == java.util.Date.class -> (rs, i, cal) -> {
Timestamp ts = rs.getTimestamp(i, cal.get());
return ts != null ? new java.util.Date(ts.getTime()) : null;
};
case Class<?> c when c == Calendar.class -> (rs, i, cal) -> {
Timestamp ts = rs.getTimestamp(i, cal.get());
if (ts == null) return null;
Calendar out = (Calendar) cal.get().clone();
out.setTimeInMillis(ts.getTime());
yield out;
}
case Class<?> c when c == Timestamp.class -> rs.getTimestamp(columnIndex, calendarSupplier.get());
case Class<?> c when c == java.sql.Date.class -> rs.getDate(columnIndex);
case Class<?> c when c == Time.class -> rs.getTime(columnIndex);
return out;
};
case Class<?> c when c == Timestamp.class -> (rs, i, cal) -> rs.getTimestamp(i, cal.get());
case Class<?> c when c == java.sql.Date.class -> (rs, i, cal) -> rs.getDate(i);
case Class<?> c when c == Time.class -> (rs, i, cal) -> rs.getTime(i);
// java.time using vendor-safe approach.
case Class<?> c when c == LocalDateTime.class -> {
Timestamp ts = rs.getTimestamp(columnIndex);
yield ts != null ? ts.toLocalDateTime() : null;
}
case Class<?> c when c == LocalDate.class -> {
java.sql.Date d = rs.getDate(columnIndex);
yield d != null ? d.toLocalDate() : null;
}
case Class<?> c when c == LocalTime.class -> {
Time t = rs.getTime(columnIndex);
yield t != null ? t.toLocalTime() : null;
}
case Class<?> c when c == Instant.class -> {
Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
yield ts != null ? ts.toInstant() : null;
}
case Class<?> c when c == OffsetDateTime.class -> {
Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
yield ts != null ? OffsetDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
}
case Class<?> c when c == ZonedDateTime.class -> {
Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
yield ts != null ? ZonedDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
}
default -> rs.getObject(columnIndex);
case Class<?> c when c == LocalDateTime.class -> (rs, i, cal) -> {
Timestamp ts = rs.getTimestamp(i);
return ts != null ? ts.toLocalDateTime() : null;
};
case Class<?> c when c == LocalDate.class -> (rs, i, cal) -> {
java.sql.Date d = rs.getDate(i);
return d != null ? d.toLocalDate() : null;
};
case Class<?> c when c == LocalTime.class -> (rs, i, cal) -> {
Time t = rs.getTime(i);
return t != null ? t.toLocalTime() : null;
};
case Class<?> c when c == Instant.class -> (rs, i, cal) -> {
Timestamp ts = rs.getTimestamp(i, cal.get());
return ts != null ? ts.toInstant() : null;
};
case Class<?> c when c == OffsetDateTime.class -> (rs, i, cal) -> {
Timestamp ts = rs.getTimestamp(i, cal.get());
return ts != null ? OffsetDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
};
case Class<?> c when c == ZonedDateTime.class -> (rs, i, cal) -> {
Timestamp ts = rs.getTimestamp(i, cal.get());
return ts != null ? ZonedDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
};
default -> (rs, i, cal) -> {
Object value = rs.getObject(i);
return rs.wasNull() ? null : value;
};
};
if (rs.wasNull()) return null;
return value;
}

@Override
Expand Down
Loading
Loading