-
Notifications
You must be signed in to change notification settings - Fork 921
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
Introduce type parameters where applicable for xDS classes. #5410
Conversation
Motivation: Following the discussion in this [comment](line#5336 (comment)), incorporating type parameters in xDS classes offers advantages such as improved type safety and enhanced readability. Modifications: - Introduce type parameters where applicable. - Split `AbstractResourceNode` into `AbstractResourceNode` and `AbstractResourceNodeWithPrimer` to accommodate the absence of a primer in the listener. - Rename `AbstractResourceHolder` to `ResourceHolderWithPrimer` for consistency due to the absence of a primer in the listener. - Conduct clean-up in some APIs. Result: - Enhanced type safety, improved readability, and a more structured representation in xDS classes.
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.
The overall direction looks good.
I prefer taking the Envoy message types as the type parameters instead of capsuled ones. If the call-site injects the innermost type, the container type can be wrapped at use-site.
*/ | ||
@UnstableApi | ||
public interface ResourceHolder { | ||
public interface ResourceHolder<T> { |
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.
public interface ResourceHolder<T> { | |
public interface ResourceHolder<T extends Message> { |
@@ -28,17 +28,17 @@ | |||
|
|||
import com.linecorp.armeria.common.annotation.Nullable; | |||
|
|||
abstract class ResourceParser { | |||
abstract class ResourceParser<T extends ResourceHolder<?>> { |
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 is still not type-safe. What do you think of taking Envoy Message
types as the parameter?
abstract class ResourceParser<T extends Message> {
@Nullable
abstract String name(T message);
abstract Class<T> clazz();
abstract ResourceHolder<T> parse(T message);
ParsedResourcesHolder<T> parseResources(List<Any> resources) {
...
}
}
final class ClusterResourceParser extends ResourceParser<Cluster> {
...
}
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.
Well the T (ResourceHolder
) is the same type that XdsStreamSubscriber
uses.
armeria/xds/src/main/java/com/linecorp/armeria/xds/DefaultResponseHandler.java
Lines 53 to 59 in 012b9d1
final XdsStreamSubscriber<T> subscriber = entry.getValue(); | |
if (holder.parsedResources().containsKey(resourceName)) { | |
// Happy path: the resource updated successfully. Notify the watchers of the update. | |
subscriber.onData(holder.parsedResources().get(resourceName)); | |
continue; | |
} |
Let me try adding one more type to ResourceParser
then.
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.
Ah... Let me check first before adding another type.
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.
Should we change the type of onData()
instead?
class XdsStreamSubscriber<T extends Message> implements SafeCloseable {
void onData(ResourceHolder<T> data) {
...
}
}
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.
Ditto I'm not sure if it's a good idea to use ResourceHolder<T>
instead of its subclasses because some of the API has its own methods. e.g. (ListenerResourceHolder.connectionManager()
, ClusterResourceHolder.upstreamTlsContext()
)
We should cast them once more.
abstract class ResourceHolderWithPrimer | ||
<SELF extends ResourceHolderWithPrimer<SELF, T, U>, T, U extends ResourceHolder<?>> | ||
implements ResourceHolder<T> { | ||
|
||
abstract SELF withPrimer(@Nullable U primer); | ||
|
||
@Nullable | ||
abstract U primer(); |
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.
How about taking the resource type as is? It would make the type signature simpler.
abstract class ResourceHolderWithPrimer | |
<SELF extends ResourceHolderWithPrimer<SELF, T, U>, T, U extends ResourceHolder<?>> | |
implements ResourceHolder<T> { | |
abstract SELF withPrimer(@Nullable U primer); | |
@Nullable | |
abstract U primer(); | |
abstract class ResourceHolderWithPrimer | |
<SELF extends ResourceHolderWithPrimer<SELF, T, U>, T extends Message, U extends Message> | |
implements ResourceHolder<T> { | |
abstract SELF withPrimer(@Nullable ResourceHolder<U> primer); | |
@Nullable | |
abstract ResourceHolder<U> primer(); |
We also need to change the concrete types with parameterized ones.
public final class RouteResourceHolder
extends ResourceHolderWithPrimer<RouteResourceHolder, RouteConfiguration, Listener> {
RouteResourceHolder(RouteConfiguration routeConfiguration, ResourceHolder<Listener> primer) {
this.routeConfiguration = routeConfiguration;
this.primer = primer;
}
...
@Override
RouteResourceHolder withPrimer(@Nullable ResourceHolder<Listener> primer) {
if (primer == null) {
return this;
}
return new RouteResourceHolder(routeConfiguration, primer);
}
@Override
@Nullable
ResourceHolder<Listener> primer() {
return primer;
}
@@ -24,7 +24,7 @@ | |||
import io.envoyproxy.envoy.config.core.v3.ConfigSource; | |||
import io.grpc.Status; | |||
|
|||
abstract class AbstractResourceNode<T> implements ResourceNode<AbstractResourceHolder> { | |||
abstract class AbstractResourceNode<T extends ResourceHolder<?>> implements ResourceNode<T> { |
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.
How about?
abstract class AbstractResourceNode<T extends ResourceHolder<?>> implements ResourceNode<T> { | |
abstract class AbstractResourceNode<T extends Message> implements ResourceNode<ResourceHolder<T>> { |
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 so, we should cast in some cases because we cannot use the concrete type.
e.g.
Can't use ClusterResourceHolder
when creating ClusterSnapshot
. Instead we should use ResourceHolder<Cluster>
.
armeria/xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceNode.java
Lines 91 to 99 in 012b9d1
final ClusterResourceHolder current = currentResourceHolder(); | |
if (current == null) { | |
return; | |
} | |
if (!Objects.equals(newSnapshot.holder().primer(), current)) { | |
return; | |
} | |
parentWatcher.snapshotUpdated( | |
new ClusterSnapshot(current, newSnapshot, virtualHost, route, index)); |
Some of the holders have its own API:
final UpstreamTlsContext tlsContext = clusterSnapshot.holder().upstreamTlsContext(); |
final class ParsedResourcesHolder<T> { | ||
private final Map<String, T> parsedResources; |
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 adding more constrains to the type parameter?
final class ParsedResourcesHolder<T> { | |
private final Map<String, T> parsedResources; | |
final class ParsedResourcesHolder<T extends Message> { | |
private final Map<String, ResourceHolder<T>> parsedResources; |
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #5410 +/- ##
===========================================
+ Coverage 0 73.94% +73.94%
- Complexity 0 20690 +20690
===========================================
Files 0 1792 +1792
Lines 0 76321 +76321
Branches 0 9720 +9720
===========================================
+ Hits 0 56439 +56439
- Misses 0 15274 +15274
- Partials 0 4608 +4608 ☔ View full report in Codecov by Sentry. |
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.
Looks clean! 👍
@@ -28,24 +28,24 @@ | |||
|
|||
import com.linecorp.armeria.common.annotation.Nullable; | |||
|
|||
abstract class ResourceParser { | |||
abstract class ResourceParser<T extends XdsResource, U extends Message> { |
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.
To explicitly express the relationship of the two parameters.
abstract class ResourceParser<T extends XdsResource, U extends Message> { | |
abstract class ResourceParser<I extends Message, O extends XdsResource> { |
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's a good idea. 👍
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.
Looks good in terms of correctness. Thanks @minwoox 🙇 👍 🙇
|
||
AbstractResourceNodeWithPrimer(XdsBootstrapImpl xdsBootstrap, @Nullable ConfigSource configSource, | ||
XdsType type, String resourceName, @Nullable ResourceHolder<U> primer, | ||
SnapshotWatcher<?> parentWatcher, ResourceNodeType resourceNodeType) { |
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 we're going to try to make everything type-safe, shouldn't parentWatcher
also be handled?
SnapshotWatcher<?> parentWatcher, ResourceNodeType resourceNodeType) { | |
SnapshotWatcher<? extends Snapshot<T>> parentWatcher, ResourceNodeType resourceNodeType) { |
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.
Because the type is not used in this class and its parent class, I didn't add it. But let me add it for clarity. 😉
@@ -79,15 +82,20 @@ boolean unregister(XdsType type, String resourceName, ResourceWatcher<AbstractRe | |||
return false; | |||
} | |||
|
|||
Map<String, XdsStreamSubscriber> subscribers(XdsType type) { | |||
return subscriberMap.getOrDefault(type, Collections.emptyMap()); | |||
<T extends XdsResource> Map<String, XdsStreamSubscriber<T>> subscribers(XdsType type) { |
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) Although it may seem like the appropriate parser, watcher, and callbacks is called, I don't think the addition of the generic parameter helps with type-safety since adding and fetching subscribers is force-cast anyways.
May I ask how the added generic parameter helps for XdsStreamSubscriber
and XdsResourceParser
helps?
What do you think of just adding the type parameter for ResourceHolder
only if you think it helps with readability?
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's a good question. In fact, this was the most challenging decision I had to make during the implementation. I considered creating four different subclasses of the XdsStreamSubscriber to avoid the unsafe cast, but I felt it might be excessive.
Although it may seem like the appropriate parser, watcher, and callbacks is called, I don't think the addition of the generic parameter helps with type-safety since adding and fetching subscribers is force-cast anyways.
As you mentioned, the appropriate parser is used here, and by checking it in just one place, we can ensure the safety of this type cast. If this change is implemented, we can be confident that all other occurrences are also safe. Without this modification, we would need to verify multiple places. I hope that clarifies the rationale.
Thanks for cleaning up! |
Motivation:
Following the discussion in this comment, incorporating type parameters in xDS classes offers advantages such as improved type safety and enhanced readability. Modifications:
AbstractResourceNode
intoAbstractResourceNode
andAbstractResourceNodeWithPrimer
to accommodate the absence of a primer in the listener.AbstractResourceHolder
toResourceHolderWithPrimer
for consistency due to the absence of a primer in the listener.