-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refine and clarify operations in the async cache implementation #2744
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a first review pass.
asyncCacheWriter = UnsupportedAsyncCacheWriter.INSTANCE; | ||
} | ||
private boolean isAsyncCacheSupportEnabled() { | ||
return this.connectionFactory instanceof ReactiveRedisConnectionFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're testing assignability against an interface that defines potentially absent types such as Mono
and Publisher
. Loading interfaces can work under certain circumstances, however depending on the runtime verifier mechanics, loading ReactiveRedisConnectionFactory
without Project Reactor can cause NoClassDefFoundError
.
Therefore, we used a safe scheme that is being used across the majority of portfolio projects and there's no reason to deviate from that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just thinking...
Perhaps the class presence check should be on a Reactive type (e.g. Mono
) then, given it does depend on the runtime verifier, particularly in strange cases where it can load the ReactiveConnectionFactory
interface, which has no explicit Reactive types until you drill into ReactiveConnection
. The real question is, is the ReactiveConnection
as a return type on ReactiveConnectionFactory
enough?
Also, by this same argument, then the only way a ReactiveConnectionFactory
can exist is if Lettuce is on the classpath anyway, which has a hard dependency on Reactor. Of course, there is no guarantee that Lettuce will always pull in Reactor as a transitive dependency (hence this), but also the reason I argued for this (internal link).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am convinced it should be both ReactiveConnectionFactory
and a Reactive type like Mono
or Flux
.
private Flux<Long> waitInterval(Duration period) { | ||
return Flux.interval(Duration.ZERO, period); | ||
} | ||
|
||
private ByteBuffer toCacheLockKey(String cacheName) { | ||
return ByteBuffer.wrap(createCacheLockKey(cacheName)); | ||
} | ||
|
||
private ReactiveRedisConnectionFactory getReactiveConnectionFactory() { | ||
return (ReactiveRedisConnectionFactory) DefaultRedisCacheWriter.this.connectionFactory; | ||
} | ||
|
||
private Mono<ReactiveRedisConnection> getReactiveConnection() { | ||
return Mono.fromSupplier(getReactiveConnectionFactory()::getReactiveConnection); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tiny methods are unnecessary and generate a lot of noise. Arguably, toCacheLockKey(String cacheName)
can be an improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree. All of them are an improvement, and for very specific reasons.
They help reduce noise in the method where they are used. Otherwise "busy" code gets bunched together causing the code formatter to break lines at critical junctures.
Shorter methods are easier to read/understand, can be intelligently named, can be referred to if need be (not unlike code folding), and are easier to test in isolation.
Plus, these private
methods can be reused, and in many places, they are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In these methods...
waitForInterval(period)
is more descriptive than Flux.interval(Duration.ZERO, period)
. What is Duration.ZERO
anyway ("delay", I know, after looking it up)? Why do I even need to know it is Flux
for that matter? It is an implementation detail. You don't know what Object.wait(..)
does when you call it and you certainly wouldn't include its logic in the calling method.
getReactiveConnectionFactory()
avoids the cast, could serve as an (internal) extension point if made package-private
, and is more descriptive than connectionFactory
alone (I am explicitly requesting a "Reactive" RedisConnectionFactory
). Additionally, this method would allow me to add an assertion requiring a ReactiveRedisConnectionFactory
and throwing an IllegalStateException
otherwise, rather than clumsily getting a possible ClassCastException
due to programmatical mistakes.
NOTE: In too many places, we refer to state variables directly rather than the appropriate getter (where applicable), preventing extensibility, something the Spring Framework excels at, and a strength I might add.
getReactiveConnection()
... Mono
from a connectionFactory.getConnection()
, Supplier
. Need I say more? Having this logic spread all over the class is redundant and busy, creating extra work in methods that need Reactive connections.
What does it matter whether it is a variable or a method? Additional (helper) methods help remove clutter from methods and can be reused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was there before was certainly less readable.
@@ -401,6 +382,14 @@ private static boolean shouldExpireWithin(@Nullable Duration ttl) { | |||
return ttl != null && !ttl.isZero() && !ttl.isNegative(); | |||
} | |||
|
|||
private Expiration toExpiration(Duration ttl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tiny methods are unnecessary and generate a lot of noise making it harder to read the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this:
return connection.stringCommands().set(wrappedKey, wrappedValue,
Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
Is easier to read than this:
stringCommands.set(wrappedKey, wrappedValue, toExpiration(ttl), SetOption.upsert())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, I have even seen things likes this:
return connection.stringCommands().set(ByteBuffer.wrap(key), ByteBuffer.wrap(value),
Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, these methods can be use as Lambda's, such as in .map(:Function<S, T>)
functions, which are useful in the context of Optionals, Streams, etc.
* @param cacheStatisticsCollector must not be {@literal null}. | ||
* @param batchStrategy must not be {@literal null}. | ||
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. | ||
* Use {@link Duration#ZERO} to disable locking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the Spring Data formatter settings. There's no point in reformatting with a different formatter as the next committer is going to apply a different format again resulting in a lot of change noise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I manually format Javadoc.
The Spring Data source code formatter for Javadoc needs to be fixed.
The excessive indentation is causing unnatural line breaks (such as sentences ending in and
, or or
, or While
, etc, are bad writing style), creating orphans, and doing other inconsistent things that do not align with the core Spring Frameworks Javadoc guidelines.
*/ | ||
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime, BatchStrategy batchStrategy) { | ||
this(connectionFactory, sleepTime, TtlFunction.persistent(), CacheStatisticsCollector.none(), batchStrategy); | ||
} | ||
|
||
/** | ||
* @param connectionFactory must not be {@literal null}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What has happened to the rest of the Javadoc? GH shows that we only document sleepTime
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only "must not be null" on @param
tags is redundant and non-informative.
In general, I'd argue if we are not going to take the time to write meaningful Javadoc, then there is no point to have Javadoc on the method at all.
Thank you for the feedback. I reviewed each comment and cited my reasoning for the requested changes. I reworked this PR to be more inline with our expectations. Specifically, I:
|
Uses more descriptive names for operations, especially Reactive operations, by either calling a local, private method or introducing a (functional) variable with strongly typed parameters. Edits and refines Javadoc. Original Pull Request: spring-projects#2717 Closes spring-projects#2743
See #2743