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

Merged
merged 31 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e050aaa
Add AsyncLoader to load and update value periodically
injae-kim Apr 10, 2024
39dc307
Address comments
injae-kim Apr 17, 2024
fcad899
Add exceptionHandler
injae-kim Apr 22, 2024
ea412c7
Add refreshIf for pre-fetch
injae-kim Apr 23, 2024
887d4a4
Refresh before check isValid to resolve timing issue
injae-kim Apr 24, 2024
cca5962
Add refresh failure log and enhance test
injae-kim Apr 24, 2024
4d917a1
Throws IllegalStateException when expiration is not set on builder
injae-kim Apr 25, 2024
97861bf
Fix refresh to load value before return
injae-kim Apr 25, 2024
5b88969
Introduce `RefreshingFuture` to easily handle refreshIf
injae-kim Apr 27, 2024
2b46072
Remove refreshExecutor
injae-kim May 8, 2024
4607a6b
Address comments
injae-kim May 8, 2024
6675ad5
Make tokenLoader inside of DefaultOAuth2AuthorizationGrant constructor
injae-kim May 8, 2024
fb5d2ce
Fix test
injae-kim May 8, 2024
af5fea9
Address comments
injae-kim May 19, 2024
6536909
Address @ikhoon's comment
injae-kim Jun 3, 2024
8533294
clean up
ikhoon Jun 4, 2024
e5ba07d
clean up 2
ikhoon Jun 4, 2024
290ed7f
Apply refreshIf to DefaultOAuth2AuthorizationGrant and add tests
ikhoon Jun 4, 2024
aa5fb87
Fix lint
injae-kim Jun 5, 2024
416075e
Address @minwoox's comment
injae-kim Jun 5, 2024
5282903
Fix test
injae-kim Jun 5, 2024
95592de
Revert @ikhoon's change
injae-kim Jun 5, 2024
cd45b6d
Remove unnecessary override join()
injae-kim Jun 5, 2024
cd0496d
Address @trustin's comment
injae-kim Jun 10, 2024
c9254c3
Address comments by @trustin
ikhoon Jun 26, 2024
39e494b
Rename AsyncLoader method name to 'load()'
ikhoon Jun 26, 2024
84645c5
fix a test bug
ikhoon Jun 26, 2024
c4f2df3
Address comments by @minwoox
ikhoon Jul 2, 2024
17f26be
Fix lint
injae-kim Jul 2, 2024
78e684e
Merge branch 'main' into async-loader
ikhoon Jul 25, 2024
e9853bb
nullaway
ikhoon Jul 25, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common.util;

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* A loader which atomically loads, caches and updates value.
*
* <p>Example usage:
* <pre>{@code
* WebClient client = WebClient.of("https://example.com");
* Function<String, CompletableFuture<String>> loader = cache -> {
* // Fetch new data from the remote server.
* ResponseEntity<String> response =
* client.prepare().get("/api/v1/items").asString().execute();
* return response.thenApply(res -> res.content());
* };
*
* AsyncLoader<String> asyncLoader =
* AsyncLoader
* .builder(loader)
* // Expire the loaded value after 60 seconds.
* .expireAfterLoad(Duration.ofSeconds(60))
* .build();
*
* // Fetch the value. This will call the loader function because the cache is empty.
* String value1 = asyncLoader.load().join();
* System.out.println("Loaded value: " + value1);
*
* // This will return the cached value because it's not expired yet.
* String value2 = asyncLoader.load().join();
* assert value1 == value2;
*
* // Wait for more than 60 seconds so that the cache is expired.
* Thread.sleep(61000);
*
* // Fetch the value again. This will call the loader function because the cache has expired.
* String value3 = asyncLoader.load().join();
* assert value1 != value3;
* }</pre>
*/
@FunctionalInterface
@UnstableApi
public interface AsyncLoader<T> {

/**
* Returns a newly created {@link AsyncLoaderBuilder} with the specified loader.
*
* @param loader function to load value. {@code T} is the previously cached value.
*/
static <T> AsyncLoaderBuilder<T> builder(
Function<@Nullable ? super T, ? extends CompletableFuture<? extends T>> loader) {
//noinspection unchecked
return new AsyncLoaderBuilder<>((Function<T, CompletableFuture<T>>) loader);
}

/**
* Returns a {@link CompletableFuture} which will be completed with the loaded value.
* A new value is fetched by the loader only if nothing is cached or the cache value has expired.
*/
CompletableFuture<T> load();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common.util;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* A builder for creating a new {@link AsyncLoader}.
*/
@UnstableApi
public final class AsyncLoaderBuilder<T> {

private final Function<@Nullable T, CompletableFuture<T>> loader;
@Nullable
private Duration expireAfterLoad;
@Nullable
private Predicate<? super T> expireIf;
@Nullable
private Predicate<? super T> refreshIf;
@Nullable
private BiFunction<? super Throwable, ? super @Nullable T,
? extends @Nullable CompletableFuture<T>> exceptionHandler;

AsyncLoaderBuilder(Function<@Nullable T, CompletableFuture<T>> loader) {
requireNonNull(loader, "loader");
this.loader = loader;
}

/**
* Expires the loaded value after the given duration since it was loaded.
* New value will be loaded by the loader function on next {@link AsyncLoader#load()}.
*/
public AsyncLoaderBuilder<T> expireAfterLoad(Duration expireAfterLoad) {
requireNonNull(expireAfterLoad, "expireAfterLoad");
checkState(!expireAfterLoad.isNegative(), "expireAfterLoad: %s (expected: >= 0)", expireAfterLoad);
this.expireAfterLoad = expireAfterLoad;
return this;
}

/**
* Expires the loaded value after the given milliseconds since it was loaded.
* New value will be loaded by the loader function on next {@link AsyncLoader#load()}.
*/
public AsyncLoaderBuilder<T> expireAfterLoadMillis(long expireAfterLoadMillis) {
checkState(expireAfterLoadMillis >= 0,
"expireAfterLoadMillis: %s (expected: >= 0)", expireAfterLoadMillis);
expireAfterLoad = Duration.ofMillis(expireAfterLoadMillis);
return this;
}

/**
* Expires the loaded value if the predicate matches.
* New value will be loaded by the loader function on next {@link AsyncLoader#load()}.
*/
public AsyncLoaderBuilder<T> expireIf(Predicate<? super T> expireIf) {
requireNonNull(expireIf, "expireIf");
this.expireIf = expireIf;
return this;
}

/**
* Asynchronously refreshes the loaded value which has not yet expired if the predicate matches.
* This pre-fetch strategy can remove an additional loading time on a cache miss.
*/
public AsyncLoaderBuilder<T> refreshIf(Predicate<? super T> refreshIf) {
requireNonNull(refreshIf, "refreshIf");
this.refreshIf = refreshIf;
return this;
}

/**
* Handles the exception thrown by the loader function.
* If the exception handler returns {@code null}, {@link AsyncLoader#load()} completes exceptionally.
*/
public AsyncLoaderBuilder<T> exceptionHandler(BiFunction<? super Throwable, ? super @Nullable T,
? extends @Nullable CompletableFuture<T>> exceptionHandler) {
requireNonNull(exceptionHandler, "exceptionHandler");
this.exceptionHandler = exceptionHandler;
return this;
}

/**
* Returns a newly created {@link AsyncLoader} with the entries in this builder.
*/
public AsyncLoader<T> build() {
return new DefaultAsyncLoader<>(loader, expireAfterLoad, expireIf, refreshIf, exceptionHandler);
}
}
Loading
Loading