Skip to content
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

Add AsyncLoader to load and update value periodically #5590

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

injae-kim
Copy link
Contributor

Fixes #5506.

Motivation:

AsyncLoader can be useful in the following situations.

  • When it is necessary to periodically read and update information from a file such as resolv.conf .
  • When data is not valid after a certain period of time, such as an OAuth 2.0 access token.

We already have an implementation for that on AbstractOAuth2AuthorizationGrant.java.
However, I hope to generalize it and add new features to use it in various cases.

Modifications:

  • Add AsyncLoader to load and update value periodically

Result:

@ikhoon ikhoon added new feature sprint Issues for OSS Sprint participants labels Apr 12, 2024
Comment on lines +109 to +113
if (token == null && fallbackTokenProvider != null) {
CompletableFuture<? extends GrantedOAuth2AccessToken> fallbackTokenFuture = null;
try {
fallbackTokenFuture = requireNonNull(
fallbackTokenProvider.get(), "fallbackTokenProvider.get() returned null");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: It is not related to this PR but fallbackTokenProvider is a name that we should only use when a token acquisition fails since it is a fallback.

I think it would be better to remove fallback from the API method. tokenProvider seems clearer.

Copy link

codecov bot commented Apr 24, 2024

Codecov Report

Attention: Patch coverage is 84.31373% with 24 lines in your changes missing coverage. Please review.

Project coverage is 74.07%. Comparing base (b8eb810) to head (cca5962).
Report is 236 commits behind head on main.

Current head cca5962 differs from pull request most recent head cd45b6d

Please upload reports for the commit cd45b6d to get more accurate results.

Files Patch % Lines
...t/auth/oauth2/DefaultOAuth2AuthorizationGrant.java 42.30% 13 Missing and 2 partials ⚠️
...necorp/armeria/common/util/DefaultAsyncLoader.java 94.33% 2 Missing and 4 partials ⚠️
...necorp/armeria/common/util/AsyncLoaderBuilder.java 85.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #5590      +/-   ##
============================================
+ Coverage     73.95%   74.07%   +0.11%     
- Complexity    20115    21252    +1137     
============================================
  Files          1730     1848     +118     
  Lines         74161    78567    +4406     
  Branches       9465    10024     +559     
============================================
+ Hits          54847    58199    +3352     
- Misses        14837    15669     +832     
- Partials       4477     4699     +222     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good.

@ikhoon ikhoon added this to the 1.29.0 milestone Jun 4, 2024
Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! 👍
Left a few comments.

}

@Override
public CacheEntry<U> join() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Do we need to override this method? cacheEntry is set to the result of the completable future anyway?

Copy link
Contributor Author

@injae-kim injae-kim Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh~ I found that we don't need to override this join() cause we call obtrudeValue(), obtrudeException() anyway inside of overrided complete(), completeExceptionally() 👍 thanks!

@@ -27,6 +27,7 @@
import org.reactivestreams.Subscription;

import com.linecorp.armeria.client.BlockingWebClient;
import com.linecorp.armeria.client.WebClient;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import com.linecorp.armeria.client.WebClient;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

95592de oh I simply remove this change~! 🙇

Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice, @injae-kim! ❤️

Please address @minwoox comments.

Comment on lines +65 to +66
* Returns a newly created {@link AsyncLoaderBuilder} with the specified loader.
* @param loader function to load value. {@code T} is previously cached value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's insert an empty line between method description and parameter list for readability.

Suggested change
* Returns a newly created {@link AsyncLoaderBuilder} with the specified loader.
* @param loader function to load value. {@code T} is previously cached value
* Returns a newly created {@link AsyncLoaderBuilder} with the specified loader.
*
* @param loader function to load value. {@code T} is previously cached value

* AsyncLoader<String> asyncLoader =
* AsyncLoader
* .builder(loader)
* .expireAfterLoad(Duration.ofSeconds(60)) // The loaded value is expired after 60 seconds.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure the line is not too long for readability.

Suggested change
* .expireAfterLoad(Duration.ofSeconds(60)) // The loaded value is expired after 60 seconds.
* // Expire the loaded value after 60 seconds.
* .expireAfterLoad(Duration.ofSeconds(60))

* A builder for creating a new {@link AsyncLoader}.
*
* <p>Expiration should be set by {@link #expireAfterLoad(Duration)} or {@link #expireIf(Predicate)}.
* If expiration is not set, {@link #build()} will throw {@link IllegalStateException}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about not throwing an ISE but just building an instance that never expires its value?

* Expires The loaded value after the duration since it was loaded.
* New value will be loaded by the loader function on next {@link AsyncLoader#get()}.
*/
public AsyncLoaderBuilder<T> expireAfterLoad(Duration expireAfterLoad) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add the expireAfterLoadMillis(long) shortcut?

private static class CacheEntry<T> {

private final T value;
private final long cachedAt = System.nanoTime();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cachedAtNanos?

Comment on lines +168 to +170
if (cacheEntry == null) {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is null an invalid entry value? A user might want to cache null as the result.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A user might want to cache null as the result.

I guess cacheEntry.value would be null in this case

}
}

private static class RefreshingFuture<U> extends CompletableFuture<CacheEntry<U>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We should not allow a user to change the state of this future via complete*() and obtrude*().
  • Can we create a new future when refreshing a new value, rather than using obtrude*()? I wouldn't risk using obtrude*() here because:
    • obtrude*() is designed by error recovery actions, which is not the case here
    • The future returned by get() can be reused by a user later; and
    • We can do without it.

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good in terms of correctness 👍 Thanks @injae-kim 🙇 👍 🙇

* @throws IllegalStateException if no expiration is set.
*/
public AsyncLoader<T> build() {
if (expireAfterLoad == null && expireIf == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional) Not too strong on this, but if CacheEntry were public then I guess it's possible to just remove expireAfterLoad and use expireIf to implement the same functionality.

This way would eliminate the ambiguity on which condition is evaluated first as well.

e.g.

AsyncLoaderBuilder<T> expireIf(Predicate<CacheEntry<? super T>> expireIf) {...}

...

    public AsyncLoaderBuilder<T> expireAfterLoad(Duration expireAfterLoad) {

...

        expireIf = cacheEntry -> {
            final long elapsed = System.nanoTime() - cacheEntry.cachedAt();
            return elapsed < expireAfterLoad.toNanos();
        };

Comment on lines +168 to +170
if (cacheEntry == null) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A user might want to cache null as the result.

I guess cacheEntry.value would be null in this case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature sprint Issues for OSS Sprint participants
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide a way to load a value periodically or when it expires
5 participants