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

[OPIK-595]: Add Redis Caching part 1 #1015

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

Conversation

thiagohora
Copy link
Contributor

Details

Add an annotation-based caching mechanism to allow us to define cacheable methods in the project.

Issues

OPIK-595

Testing

Adding Integration tests to the new mechanism

@thiagohora thiagohora requested a review from a team as a code owner January 9, 2025 17:35
@thiagohora thiagohora force-pushed the thiagohora/OPIK-595_redis_caching_part_1 branch from c9df8cc to 7b723b1 Compare January 9, 2025 17:36
@thiagohora thiagohora force-pushed the thiagohora/OPIK-595_redis_caching_part_1 branch from 7b723b1 to 1247a9b Compare January 9, 2025 17:37
@thiagohora thiagohora self-assigned this Jan 9, 2025
@thiagohora thiagohora force-pushed the thiagohora/OPIK-595_redis_caching_part_1 branch from dbeeb59 to 593da4a Compare January 10, 2025 10:57
Copy link
Collaborator

@andrescrz andrescrz left a comment

Choose a reason for hiding this comment

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

It's admirable that you went ahead with an implementation that tries to solve the problem in a general way, beyond just caching the automation rules.

This is also heavily based on the Spring Cache approach, which provides an interface with known annotations (@Cacheable, @CachePut and @CacheEvict).

However, this has many non-trivial edge cases and building a library like this is a problem by itself that requires a lot of focus. In our case it has to be just good enough, but already has some challenges: reactive vs non-reactive methods, distinguishing between types (collections and non-collections currently) etc. and this moves us away from focusing on the business cases strictly related to Opik.

I was wondering if you investigated some alternative that we can just use. Some examples:

  1. Something in Dropwizard.
  2. Some Java Library than can be wrapped around the Redis.
  3. Integrating some parts of Spring Cache.

I'd be fine to move forward, but it's desirable to investigate this first, in order to keep focusing on building Opik use cases.

@@ -198,6 +198,16 @@
<artifactId>httpclient5</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: we could evaluate using Drools as well. But this is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you believe Drools, it's better that we use it.

@@ -157,6 +157,7 @@ public <E, T extends AutomationRuleEvaluator<E>> T findById(@NonNull UUID id, @N
case LlmAsJudgeAutomationRuleEvaluatorModel llmAsJudge ->
AutomationModelEvaluatorMapper.INSTANCE.map(llmAsJudge);
})
.map(evaluator -> (T) evaluator)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: for a safer cast, better check if instance of in a filter. Anyhow, it might not ever happen or maybe it's ok to allow this error to bubble up if it ever happens an unexpected programming error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, given the switch, I imagine this would not happen, but Sonar keeps complaining. I changed it so it only complains in one line instead of the whole method

Comment on lines 29 to 30
private final Provider<CacheManager> cacheManager;
private final CacheConfiguration cacheConfiguration;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: missing non-null validation on these fields. Applies to the whole PR. Also, add the same in method arguments, whenever is possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return invocation.proceed();
}

boolean isReactive = method.getReturnType().isAssignableFrom(Mono.class);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this cover Flux as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, as I said, I tried not to go too far, but once we need it, we can implement it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

key = getKeyName(name, keyAgs, invocation);
} catch (Exception e) {
// If there is an error evaluating the key, proceed without caching
log.warn("Cache will be skipped due to error evaluating key expression");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please log the exception as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error is logged on getKeyName

Comment on lines 76 to 80
if (isReactive) {
return action.apply(key, name);
}

return action.apply(key, name).block();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you avoid this distinction and return the result of applying the action without blocking? Would it work by delegating the block call to the handler of this result?

Copy link
Contributor Author

@thiagohora thiagohora Jan 10, 2025

Choose a reason for hiding this comment

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

This code would fail if the method annotated returns a non-reactive type. Otherwise, we would restrict the annotation of reactive methods.

}

private Mono<Object> processCacheEvictMethod(MethodInvocation invocation, boolean isReactive, String key) {
if (isReactive) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The same applies for all the other methods. We should try to have a general implementation, that doesn't distinguish by reactive or not. Otherwise, this library won't work for other frameworks. Imagine that we use RxJava.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I would love to do that, but we need to know the type to handle the cache properly without missing the business logic.


if (isReactive) {

if (cacheable.collectionType() != Collection.class) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The same applies for collection class. It'd be great to not to branch depending on the cacheable type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but the issue is that I need to know how to deserialize the cache, to do this without the check, I would need somehow to keep the generic types at runtime, and this is not possible without some compile-time magic. Maybe we can implement it in the future, but I wouldn't go that far in the first iteration.

Comment on lines +66 to +68
new CustomConfig("cacheManager.enabled", "true"),
new CustomConfig("cacheManager.defaultDuration", "PT0.500S"),
new CustomConfig("cacheManager.caches.%s".formatted(CACHE_NAME_2), "PT0.200S")))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just configure these values in the test config yaml file instead of overwriting them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would keep the cache disabled by default and let the specific test enable it. Otherwise, the side effects may impact other tests.

@thiagohora
Copy link
Contributor Author

It's admirable that you went ahead with an implementation that tries to solve the problem in a general way, beyond just caching the automation rules.

This is also heavily based on the Spring Cache approach, which provides an interface with known annotations (@Cacheable, @CachePut and @CacheEvict).

However, this has many non-trivial edge cases and building a library like this is a problem by itself that requires a lot of focus. In our case it has to be just good enough, but already has some challenges: reactive vs non-reactive methods, distinguishing between types (collections and non-collections currently) etc. and this moves us away from focusing on the business cases strictly related to Opik.

I was wondering if you investigated some alternative that we can just use. Some examples:

  1. Something in Dropwizard.
  2. Some Java Library than can be wrapped around the Redis.
  3. Integrating some parts of Spring Cache.

I'd be fine to move forward, but it's desirable to investigate this first, in order to keep focusing on building Opik use cases.

Yeah, I looked for something similar to Spring Cache but couldn't find it. The closest one was https://github.com/bazaarvoice/dropwizard-caching-bundle, but it's archived. I also found some others using ehcache and caffeine, but all seem old and without maintenance.

https://github.com/gabber12/dropwizard-caffeine-configurator

Anyway, I agree with you it's definitely not a trivial problem, but I didn't try to implement a solution to all scenarios. Instead,d I tried to go with something small that would cover our basic scenario, and if needed, we could continue building it.

@thiagohora thiagohora requested a review from andrescrz January 10, 2025 11:52
@andrescrz
Copy link
Collaborator

It's admirable that you went ahead with an implementation that tries to solve the problem in a general way, beyond just caching the automation rules.
This is also heavily based on the Spring Cache approach, which provides an interface with known annotations (@Cacheable, @CachePut and @CacheEvict).
However, this has many non-trivial edge cases and building a library like this is a problem by itself that requires a lot of focus. In our case it has to be just good enough, but already has some challenges: reactive vs non-reactive methods, distinguishing between types (collections and non-collections currently) etc. and this moves us away from focusing on the business cases strictly related to Opik.
I was wondering if you investigated some alternative that we can just use. Some examples:

  1. Something in Dropwizard.
  2. Some Java Library than can be wrapped around the Redis.
  3. Integrating some parts of Spring Cache.

I'd be fine to move forward, but it's desirable to investigate this first, in order to keep focusing on building Opik use cases.

Yeah, I looked for something similar to Spring Cache but couldn't find it. The closest one was https://github.com/bazaarvoice/dropwizard-caching-bundle, but it's archived. I also found some others using ehcache and caffeine, but all seem old and without maintenance.

https://github.com/gabber12/dropwizard-caffeine-configurator

Anyway, I agree with you it's definitely not a trivial problem, but I didn't try to implement a solution to all scenarios. Instead,d I tried to go with something small that would cover our basic scenario, and if needed, we could continue building it.

Discussed offline, but leaving my proposal here as well. However, it requires some investigation from your side:

In Java there's a standard API for caching, it's name is JCache. Actually, Spring Cache is pretty similar to it, many classes and even the annotations have the same or equivalent names.

We use Redisson as Redis client, which I believe provides an implementation for JCache. I think there's a lot of overlap with your implementation, including the annotations and some classes or interfaces such as CacheManager.

Additionally, Redisson supports the reactive API and I think they have support for that in their JCache implementation.

I think there are chances that your implementation would be greatly simplified, an the problems that I'm pointing out in my review would go away (or mostly) with this approach.

I'm not completely sure, but it's likely to be a matter of glueing things together: Redisson and its JCache implementation, with our use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants