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

Make the config object model read-only #662

Merged
26 commits merged into from
Jan 26, 2021
Merged

Make the config object model read-only #662

26 commits merged into from
Jan 26, 2021

Conversation

Tratcher
Copy link
Member

@Tratcher Tratcher commented Jan 13, 2021

Fixes #179

Today Cluster, ProxyRoute, etc. are mutable objects used to build the initial data model. Because these are mutable we clone them several times to prevent anyone from modifying them when we don't expect it, such as before calling user callbacks.

To avoid extra copies we want to make the input object model immutable but still have it be easy to construct. Here I've experimented with using C# 9 record types to achieve that. A record type is an immutable class with automatically generated methods like Clone and Equals. Using init only properties makes the objects still easy to construct. The new with operator allows making mutated clones of records so we can still manipulate the objects when we need to but not worry about someone else changing them unexpectedly.

There's a lot of churn-in-place here, but I'm happy with the results. All DeepClones were removed and I was able to remove many redundant classes from RuntimeModel that were only there as read-only copies of the config model.

@@ -16,11 +16,11 @@ public interface ITransformBuilder
/// Validates that each transform is known and has the expected parameters. All transforms are validated and
/// so all errors can be reported.
/// </summary>
IList<Exception> Validate(IList<IDictionary<string, string>> transforms);
IList<Exception> Validate(IReadOnlyList<IReadOnlyDictionary<string, string>> transforms);
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks crazy 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. We'll see if I can sort it out as part of #60 (coming soon).

@davidfowl
Copy link
Contributor

I like it!

Copy link
Member

@ManickaP ManickaP left a comment

Choose a reason for hiding this comment

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

Not a full pass through the change. I'll continue on Monday.

src/ReverseProxy/Abstractions/Config/IProxyConfigFilter.cs Outdated Show resolved Hide resolved
/// </summary>
public static ProxyRoute WithTransformPathSet(this ProxyRoute proxyRoute, PathString path)
{
return proxyRoute.WithTransform(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
Copy link
Member

Choose a reason for hiding this comment

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

When initializing from the config we went an extra mile and every Dictionary/List properly wrapped in their ReadOnly counterpart. But here we're forgoing that and just passing Dictionary itself.
So either we do care that everything is properly readonly and nobody cannot even cast the instance to non-readonly counterpart. Or we can quite substantially simplify the config reading.
Or is there any other argument why is it here more lax?

Copy link
Member Author

Choose a reason for hiding this comment

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

There is some awkwardness around IDictionary vs IReadOnlyDictionary vs Dictionary. Dictionary implements both interfaces, but IDictionary does not implement IReadOnlyDictionary and requires wrapping. Many of the config and linq APIs only return an IDictionary.

Dictionary is easy to use here and we don't have to worry about it being modified because nobody else ever has access to it without downcasting or other tricks. I can try a ReadOnlyDictionary to see if that's as easy.

@ManickaP
Copy link
Member

Will this change have any effect on the runtime model (RouteInfo, RouteConfig) or not? Or this this just to get rid of all the DeepClone stuff?

@Tratcher
Copy link
Member Author

Will this change have any effect on the runtime model (RouteInfo, RouteConfig) or not? Or this this just to get rid of all the DeepClone stuff?

The main goal is to get rid of the DeepClone code. I don't expect many changes to the runtime model, but we may be able to remove some redundancy.

@Tratcher Tratcher force-pushed the tratcher/freeze branch 2 times, most recently from 944f3a9 to e38a95e Compare January 21, 2021 17:17
@Tratcher
Copy link
Member Author

Will this change have any effect on the runtime model (RouteInfo, RouteConfig) or not? Or this this just to get rid of all the DeepClone stuff?

The main goal is to get rid of the DeepClone code. I don't expect many changes to the runtime model, but we may be able to remove some redundancy.

There was actually a lot of redundancy on ClusterConfig I was able to remove. There were lots of Cluster* types that were read-only copies original config that we didn't need anymore.

@Tratcher Tratcher added this to the YARP 1.0.0-preview9 milestone Jan 22, 2021
@Tratcher Tratcher marked this pull request as ready for review January 22, 2021 00:28
Copy link
Member

@ManickaP ManickaP left a comment

Choose a reason for hiding this comment

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

Look cleaner with the records!

@@ -329,5 +338,13 @@ private static bool ContainsKey(string expectedKeyName, ReadOnlySpan<char> actua
keyRemainder = actualKey.Slice(expectedKeyName.Length);
return true;
}

private class RouteHeaderFields
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of this extra class but I guess there's no reasonable way around it unfortunately.

@@ -23,49 +21,22 @@ public sealed class ClusterConfig
{
public ClusterConfig(
Copy link
Member

Choose a reason for hiding this comment

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

Could we even get rid of ClusterConfig? Maybe not necessarily as part of this PR, but in general.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we still need it. It serves as an atomic unit of state that's updated on the ClusterInfo and snapshotted with requests.

@Tratcher Tratcher added the Auto-Merge Apply this label and the msftbot will auto-merge this PR when it is eligible for merge label Jan 26, 2021
@ghost
Copy link

ghost commented Jan 26, 2021

Hello @Tratcher!

Because this pull request has the Auto-Merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ghost ghost merged commit 0b314e1 into master Jan 26, 2021
@ghost ghost deleted the tratcher/freeze branch January 26, 2021 21:07
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auto-Merge Apply this label and the msftbot will auto-merge this PR when it is eligible for merge Breaking-Change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use freezable pattern for contract types instead of deep cloning
5 participants