markAsRemoved() transitions a node to the removed state and decrements
the current size, but it did not check whether the node had already
been removed. The eviction path and an explicit removal can process
the same node in sequence: when a write drain runs a queued AddTask
whose eviction polls a node that a concurrent remove(K) has already
taken out of the cache, the eviction decrements the size, and the
queued RemovalTask for the same node decrements it again. The sibling
transition markForRemoval() guards against invalid transitions; this one
did not.
Each extra decrement makes currentSize permanently smaller than the
number of cached entries, so eviction stops triggering and the cache
exceeds its capacity for good, silently. A bounded two-thread stress
run accumulates the drift reliably: before the change the cache
stabilized far above its capacity in 20 out of 20 runs.
markAsRemoved() now returns without decrementing when the entry is
already in the removed state, mirroring the guard in markForRemoval().
The removed state is terminal, so each node is counted down exactly
once. The new test races explicit removals against eviction and then
verifies that the cache converges back to its capacity; it also
asserts that the racing thread ran and terminated cleanly.
Closes gh-37268
Signed-off-by: junhyeong9812 <pickjog@gmail.com>
When a column was declared via usingColumns() and also listed in
usingGeneratedKeyColumns(), TableMetaDataContext.reconcileColumnsToUse
accepted the declared list as-is: the generated key column was rendered
into the INSERT statement and counted against the parameter values,
even though the database is expected to generate its value.
Such an overlap is a configuration error, so it is now rejected at
compile time with an InvalidDataAccessApiUsageException naming the
offending columns in their declared spelling, consistent with the
existing validation in AbstractJdbcInsert.compile(). Matching is
case-insensitive, mirroring the normalization used for auto-discovered
columns; the auto-discovery path itself is unchanged and continues to
exclude generated key columns silently.
The tests cover the rejection, its message, a case-insensitive variant,
and the untouched non-overlapping declared path.
Closes gh-37014
Signed-off-by: junhyeong9812 <pickjog@gmail.com>
OptionalToObjectConverter.matches() used
TypeDescriptor.getElementTypeDescriptor(), which returns null for an
Optional (element types are only resolved for arrays, streams and
collections). ConversionUtils.canConvertElements() then treats a null
source element type as "maybe" and returns true unconditionally, so
ConversionService.canConvert(Optional<X>, target) reported true even
when X is not convertible to the target -- a violation of the
canConvert contract, since the subsequent conversion fails.
To address that, this commit resolves the Optional's element type from
its generic and checks it against the target, mirroring
ObjectToOptionalConverter. A raw or otherwise unresolved element type
remains permissive.
Closes gh-36913
Signed-off-by: junhyeong9812 <pickjog@gmail.com>
Property.resolveName() located the get/is accessor prefix with
String.indexOf, which matches the prefix anywhere in the method name.
A plain accessor whose name embeds such a prefix (for example
budget()) had the wrong portion stripped and resolved to an empty or
wrong property name, which in turn caused the backing field's
annotations to be silently dropped.
Match the get/is prefix only at the start of the method name and do
not strip it when the method is a plain accessor for a data class,
that is, a non-static no-arg method referring to an instance field of
the same name. This supports Java records, Kotlin data classes, and
custom Java data classes alike, without relying on java.lang.Record.
As a consequence, a getter backed by a field of the exact same name
(for example isUrgent()) now resolves to the field name.
Closes gh-36911
Signed-off-by: junhyeong9812 <pickjog@gmail.com>