-
Notifications
You must be signed in to change notification settings - Fork 122
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
Cache project metadata and tokens #1099
base: main
Are you sure you want to change the base?
Conversation
Motivation: Currently, only the raw contents of `ProjectMetadata` and `Tokens` are cached when fetched from `MetadataService`. However, deserialization takes a significant amount of time, so caching the deserialized instances would improve performance. Modifications: - Renamed `CacheableCall` to `AbstractCacheableCall`. - Introduced `CacheableCall` interface. - Added `Repository.execute(CacheableCall)` to cache calls. - Cached deserialized instances of `ProjectMetadata` and `Tokens` retrieved by `MetadataService`. Result: - Improved performance by avoiding redundant deserialization of `ProjectMetadata` and `Tokens`.
entry -> convertWithJackson(entry, ProjectMetadata.class)); | ||
tokenRepo = new RepositorySupport<>(projectManager, executor, | ||
entry -> convertWithJackson(entry, Tokens.class)); | ||
metadataRepo = new RepositorySupport<>(projectManager, executor, ProjectMetadata.class); |
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 fact, ProjectMetadata
is already cached in DefaultProject
and automatically refreshed when the raw data is updated.
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 do you think of replacing the implementation of fetchMetadata0()
with projectManager.get(projectName).metadata()
?
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 can't replace it completely because fetching is still used here:
centraldogma/server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java
Line 170 in b9cebef
return fetchMetadata0(projectName); |
Let me add
getMetdata
instead. 😉
server/src/main/java/com/linecorp/centraldogma/server/metadata/Roles.java
Show resolved
Hide resolved
return unsafeCast(cache.get(cacheableCall).handleAsync((unused, cause) -> { | ||
throwUnsafelyIfNonNull(cause); | ||
return unused; |
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.
It doesn't seem like that unused
is a proper name.
return unsafeCast(cache.get(cacheableCall).handleAsync((unused, cause) -> { | |
throwUnsafelyIfNonNull(cause); | |
return unused; | |
return unsafeCast(cache.get(cacheableCall).handleAsync((value, cause) -> { | |
throwUnsafelyIfNonNull(cause); | |
return value; |
// Cached already. | ||
return existingValue; | ||
} | ||
final CompletableFuture<T> future = new CompletableFuture<>(); |
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.
Question) Should we check if the cache was populated by a previous call first before creating a new future?
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.
Creating a future is cheaper than calling Cache.getIfPresent
.
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.
This approach is recommended by the team. (Sorry for not giving you enough information.)
https://github.com/ben-manes/caffeine/wiki/Faq#recursive-computations
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 would like to know what your reasoning is. Could you elaborate on that? Is .getIfPresent
more expensive cost than .putIfAbsent
?
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.
If you want to check if the cache was popolulated, you need to call getIfPresent
first and you need to call put
or putIfAbsent
if it's not created.
But in this approach, we can just call putIfAbsent
only once. Did I miss something?
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 understand your intention. There is a trade-off for that. If the cache hit ratio is high, most calls will be handled by getIfPresent()
.
As the change is trivial, let's keep it as it. I just wondered the reason. :-)
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.
That is also true.
Creating a future is relatively cheap and the code is shorter so I prefer this way. 😉
entry -> convertWithJackson(entry, ProjectMetadata.class)); | ||
tokenRepo = new RepositorySupport<>(projectManager, executor, | ||
entry -> convertWithJackson(entry, Tokens.class)); | ||
metadataRepo = new RepositorySupport<>(projectManager, executor, ProjectMetadata.class); |
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 do you think of replacing the implementation of fetchMetadata0()
with projectManager.get(projectName).metadata()
?
Suggestion) It would be worth caching |
That's a good suggestion. Let me handle it in this PR. 👍 |
@ikhoon |
Let's do 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.
LGTM once the CI builds pass. 👍
|
||
private final Map<String, CompletableFuture<Revision>> reposInAddingMetadata = new ConcurrentHashMap<>(); | ||
|
||
/** | ||
* Creates a new instance. | ||
*/ | ||
public MetadataService(ProjectManager projectManager, CommandExecutor executor) { | ||
public MetadataService(ProjectManager projectManager, CommandExecutor executor, |
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 reveiew this class with the hiding whitespace mode. 😉
I put the patches in this PR. 😓 |
if (loginUser.isSystemAdmin()) { | ||
return mds.getTokens() | ||
.thenApply(tokens -> tokens.appIds().values()); | ||
return mds.getTokens().appIds().values(); |
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 can simply use the cached tokens in this case.
@@ -127,12 +123,12 @@ public CompletableFuture<HttpResult<Token>> createToken(@Param String appId, | |||
tokenFuture = mds.createToken(author, appId, isSystemAdminToken); | |||
} | |||
return tokenFuture | |||
.thenCompose(unused -> mds.findTokenByAppId(appId)) | |||
.thenCompose(unused -> fetchTokensByAppId(appId)) |
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 have to fetch the tokens because the tokens are just updated.
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.
Looked through the code, and the direction looks good to me.
I didn't check cases where a create directly leads to a get - hopefully the following comment is addressed before RepositoryListener
is used more extensively
ref: #1053 (comment)
Motivation:
Currently, only the raw contents of
ProjectMetadata
andTokens
are cached when fetched fromMetadataService
. However, deserialization takes a significant amount of time, so caching the deserialized instances would improve performance.Modifications:
CacheableCall
interface.Repository.execute(CacheableCall)
to cache calls.ProjectMetadata
andTokens
retrieved byMetadataService
.CacheableCompareTreesCall
.Result:
ProjectMetadata
andTokens
.