-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Non-static SerializationManager #2592
Conversation
great work @ReubenBond ! |
services.AddSingleton<RoslynCodeGenerator>(); | ||
services.AddSingleton<SerializationManager>(); | ||
var serviceProvider = services.BuildServiceProvider(); | ||
var codeGenerator = serviceProvider.GetRequiredService<RoslynCodeGenerator>(); |
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 seems like a quite peculiar way of constructing a RoslynCodeGenerator. Would you comment on your reasoning?
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.
Oh yeah, it is not what DI is for ;-) totally agreeing with @jason-bragg!
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's peculiar about it?
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.
Seems peculiar to construct a container to construct a class rather than just constructing the class directly.
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.
There's some ugliness around this, by the way, since we don't link OrleansRuntime against the code generator and we have this CodeGeneratorManager
type which tries to perform the linking at runtime. CodeGeneratorManager
uses RoslynCodeGenerator
, but in this revision of the PR, it will use a different instance. I've fixed that locally, so it will use the container instance if it's available, but it's still ugly.
I'll see if I can remove AssemblyProcessor
and CodeGeneratorManager
from this project
@@ -79,23 +80,22 @@ internal class AssemblyLoader | |||
return discoveredAssemblyLocations; | |||
} | |||
#endif | |||
public static T TryLoadAndCreateInstance<T>(string assemblyName, Logger logger) where T : class | |||
public static T TryLoadAndCreateInstance<T>(string assemblyName, IServiceProvider serviceProvider, Logger logger) where T : 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.
Suggestion - feel free to ignore, as this can be done later.
Suggest introducing a generic type factory interface and passing it in rather than the entire IServiceProvider. Something like:
interface ITypeFactory
{
T Create(Type concreteType) where T : class;
}
An impl of this that calls the activator utilities with the service provider can be passed in making this call path more specific and not depend on DI.
Again, this can be done as a minor refactor later..
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 think we should leave this for later, as we discussed. I've removed all new abstractions from this PR so that it's more focused on the main goal.
|
||
public CodeGeneratorManager(IServiceProvider serviceProvider) | ||
{ | ||
this.serviceProvider = serviceProvider; |
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.
ITypeFactory again. In this case, it could also be registered in the service provider to allow it to be injected into the code generator manager
/// <summary> | ||
/// Gets or sets the service configuration delegate. | ||
/// </summary> | ||
public Func<IServiceCollection, IServiceProvider> ServiceConfigurator { get; set; } = services => services.BuildServiceProvider(); |
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 a general sense this is functionality not configuration. Does this belong here?
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 should remove it from this PR and we can talk about introducing it later. It's a good hook to have, and I consider it configuration since it's used only as part of the configuration phase and is the same signature as our Startup.ConfigureServices method (& same as ASP.NET), but it's not serializable.
src/Orleans/Core/GrainClient.cs
Outdated
@@ -327,7 +331,7 @@ public static Logger Logger | |||
get | |||
{ | |||
CheckInitialized(); | |||
return RuntimeClient.Current.AppLogger; | |||
return ((IRuntimeClient)outsideRuntimeClient).AppLogger; |
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.
Why is cast necessary here?
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.
OutsideRuntimeClient explicitly implements IRuntimeClient for AppLogger
} | ||
|
||
#endregion | ||
|
||
public void Bind(IGrainFactory factory) |
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 don't see where this is used? Concerned that a grain reference read from storage may not be bound prior to being used in a grain call.
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 should remove this method and add it as an extension method in a separate PR. There's a #warning
you can search for to see where the runtime client is set during deserialization. Currently, references to foreign clusters will always be deserialized as though they are references to the cluster whose SerializationManager is deserializing it.
} | ||
else if (systemTargetIndex >= 0) | ||
{ | ||
grainIdStr = trimmed.Substring(grainIdIndex, systemTargetIndex - grainIdIndex).Trim(); | ||
string systemTargetStr = trimmed.Substring(systemTargetIndex + (SYSTEM_TARGET_STR + "=").Length); | ||
SiloAddress siloAddress = SiloAddress.FromParsableString(systemTargetStr); | ||
return FromGrainId(GrainId.FromParsableString(grainIdStr), null, siloAddress); |
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 these have null runtime, who ensures they get bound?
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.
There's a #warning on this method in my local branch - it needs to be fixed. Trying to use a grain ref which is not bound will throw a well-typed exception
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 still need to work out the best approach. The simplest is to add a GetGrainFromKeyString(string)
method to IGrainFactory
or a new service and to move this method there.
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 is resolved - no nulls.
this.InternalGrainFactory = this.ServiceProvider.GetRequiredService<IInternalGrainFactory>(); | ||
this.ClientStatistics = this.ServiceProvider.GetRequiredService<ClientStatisticsManager>(); | ||
this.SerializationManager = this.ServiceProvider.GetRequiredService<SerializationManager>(); | ||
this.messageFactory = this.ServiceProvider.GetService<MessageFactory>(); |
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.
As before, peculiar construction pattern.
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 don't think so, consider that:
- We prefer to have just one instance of this class (really any stateless class)
- This class relies on some services in the container
- Some services in the container rely on this 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.
As with earlier comment, I'm not suggesting this is wrong or should be changed, just trying to understand the reasoning for this pattern.
In this case I can at least see that the service provider is kept around for future use, but it seems to only be needed for the StreamProviderManager.
Suggest container construction be separated and passed to the OutsideRuntimeClient constructor for testability reasons. Can even be a static factory function if needed.
static IServiceProvider DefaultServiceProvider(ClientConfiguration cfg)
{
var services = new ServiceCollection();
....
return services.BuildServiceProvider();
}
public OutsideRuntimeClient(IServiceProvider sp, ClientConfiguration cfg, bool secondary)
{
...
}
public OutsideRuntimeClient(ClientConfiguration cfg, bool secondary = false)
: this(DefaultServiceProvider(cfg), cfg, secondary)
{
...
}
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.
Suggest container construction be separated and passed to the OutsideRuntimeClient constructor for testability reasons.
Yeah, I'm not entirely happy with the way the container is constructed today - either here or in Silo.cs (also, it's not even consistent: why is it not constructed in InsideRuntimeClient
if here it's in OutsideRuntimeClient
, seems illogical, right?)
I see it as a post-PR topic, since we should change both and move towards a 'SiloBuilder'/'ClientBuilder' approach. I'm certainly not firm on that, but I'd like to have a separate discussion on what the root of our system is. Currently, I'd say it's the IRuntimeClient
implementation, but IRuntimeClient
is internal and so users cannot make use of it.
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.
Understood. Huge PR, so I'm quite ok with pushing this to a later refactor. Just wanted to make sure I understood why it was this way. Having said that, the more of this that you can structure well in this pass, the more well formed the code will be if we don't get back to this immediately. I'm not confident we've a good record of doing cleanup passes yet.
142ecb6
to
5d5cbde
Compare
1e37449
to
5fafab1
Compare
src/Orleans/Core/GrainExtensions.cs
Outdated
/// </summary> | ||
/// <param name="grain">The grain reference.</param> | ||
/// <param name="grainFactory">The grain factory.</param> | ||
public static void BindGrainReference(this IAddressable grain, IGrainFactory grainFactory) |
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 is a convenience method to allow grains to be bound to the grain factory. As you can see, it's trivial. Should it be removed?
43ab123
to
ce7c9a8
Compare
@@ -29,16 +30,28 @@ public interface IMemoryMessageBodySerializer | |||
/// Default IMemoryMessageBodySerializer | |||
/// </summary> | |||
[Serializable] | |||
public class DefaultMemoryMessageBodySerializer : IMemoryMessageBodySerializer | |||
public class DefaultMemoryMessageBodySerializer : IMemoryMessageBodySerializer, IOnDeserialized |
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.
After looking at this a bit more, I think a better solution would be for the [de]serialization calls in the IMemoryMessageBodySerializer to take a serialization manager as an argument, and for the GetEvents call in the IBatchContainer to also take a serialization manager. The stream consumer extension getting the messages from the batch container would need to pass in a serialization manager. This, IMO, is a more straight forward approach.
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.
@jason-bragg I'm happy with that approach. I didn't want to change the interface, since it implies that it's supposed to be serializer agnostic.
I don't want to have to add IOnDeserialized
just for this.
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.
Understood. I'd also prefer a solution that didn't alter the public surface, as this will be a breaking change, but I'm more open to breaking changes in the streaming system as that is still one of the newer systems which is, imo, still in flux. We can also probably count the number of users that implemented custom IBatchContainers on one hand...
c227f04
to
6b7eacc
Compare
e5995ff
to
24e94d6
Compare
76f4c07
to
411ef9f
Compare
411ef9f
to
46d1e81
Compare
I'm not sure if I had a comment on a previous PR or it was here and got lost, but this exception message needs an update, especially since it's a sneaky breaking change that would not break at compile time. Refers to: src/Orleans/Serialization/SerializationManager.cs:634 in 975bdbb. [](commit_id = 975bdbb4fbfb5e7d8504df67bd6880a16803bbc2, deletion_comment = False) |
|
||
void IOnDeserialized.OnDeserialized(ISerializerContext context) | ||
{ | ||
this.serializer = ActivatorUtilities.GetServiceOrCreateInstance<TSerializer>(context.ServiceProvider); |
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.
Awesome, this is what I was advocating for. Nevertheless, for performance reasons, I think there is a method in ActivatorUtilities that allows you to create an object factory and cache it, and avoid reflection in subsequent calls, since this is going to be called extremely frequently for these same types
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 updated method would look like this:
this.serializer = context.ServiceProvider.GetService<TSerializer>() ?? (TSerializer)ObjectFactory(context.ServiceProvider, null);
Where ObjectFactory is
private static readonly ObjectFactory ObjectFactory = ActivatorUtilities.CreateFactory(
typeof(TSerializer),
null);
Sound good?
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.
sounds good, although I'd make this field Lazy<ObjectFactory>
so that exceptions flow to the right place, and not on type initialization.
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.
BTW, I'd call this field serializerFactory, not ObjectFactory.
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.
Sure thing
|
||
void IOnDeserialized.OnDeserialized(ISerializerContext context) | ||
{ | ||
this.serializationManager = context.SerializationManager; |
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.
correct me if I'm wrong, but with the latest, this class is never (de)serialized, so the IOnDeserialized interface is not needed here. It will always be constructed using ActivatorUtilities. In theory you could also drop the [Serializable] attribute from it (it wasn't being used before either). Unless you added this in case someone was incorrectly using it and serializing it for a different purpose?
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.
Unless you added this in case someone was incorrectly using it and serializing it for a different purpose?
precisely :)
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'm happy with these changes, I believe the IOnDeserialized interface is an elegant solution.
How does this sound for an updated error message? "Type {0} from assembly {1} has the RegisterSerializer attribute but no public static void Register(SerializationManager sm) method." |
Message looks good. We might want to assert if the old signature was used instead, and provide a more informative message, but I'm fine not doing this extra work. |
I think we should make Roslyn Analyzers for these cases - it's a much better experience |
@@ -79,6 +72,7 @@ public void Init(IProviderConfiguration providerConfig, string name, Logger log, | |||
|
|||
// 10 meg buffer pool. 10 1 meg blocks | |||
bufferPool = new FixedSizeObjectPool<FixedSizeBuffer>(adapterConfig.CacheSizeMb, () => new FixedSizeBuffer(1 << 20)); | |||
this.serializer = ActivatorUtilities.CreateInstance<TSerializer>(this.serviceProvider); |
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.
hmmm, this is creating the serializer differently than in MemoryBatchContainer...
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.
Consolidated in latest
@@ -79,6 +77,9 @@ public void Init(IProviderConfiguration providerConfig, string name, Logger log, | |||
|
|||
// 10 meg buffer pool. 10 1 meg blocks | |||
bufferPool = new FixedSizeObjectPool<FixedSizeBuffer>(adapterConfig.CacheSizeMb, () => new FixedSizeBuffer(1 << 20)); | |||
|
|||
this.serializer = this.serviceProvider.GetService<TSerializer>() ?? | |||
(TSerializer)ObjectFactory.Value(this.serviceProvider, null); |
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.
nit: I'd avoid duplication (which should always match) by extracting this to an internal static CreateSerializer(IServiceProvider) method, and then the MemoryBatchContainer just calls MemoryAdapterFactory.CreateSerializer
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 figured duplication was cheaper, but I'll change it.
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.
Does the latest commit look like what you're after?
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.
absolutely, thanks
f6e78dc
to
dfc27e1
Compare
LG2M |
dfc27e1
to
5a9d837
Compare
Woohooo!! this is in! 🎉 Thanks @ReubenBond for bearing with us! :) |
I'm so happy :D Thanks for putting up with it - it was a long road and a big PR, but this lays the groundwork for more modularity as well as #467 |
Shipit! |
This was yet another epic thing by Reuben! |
Disclaimer: this is a work in progress. It needs work, more functional testing, and to be split into smaller, more mergable PRs.
SerialziationManager
is a core part of our system andGrainReference
is serializable, so this PR is necessarily going to be largeImplements changes from #2591 & #467 (with changes).
When using an external serializer (eg, JSON.NET, ProtoBuf),
GrainReference
instances must be bound via a call toIGrainFactory.Bind(IAddressable)
before they can be used. When using the Orleans serialization framework, this happens automatically.Once this is ready, we will be close to completing #467. Only some minor additional work is required to complete the task, basically changing the GrainClient class itself.
Note: this is a breaking change.
EDIT: to be clear, I don't expect this code to be reviewed in detail while I've got the work-in-progress tag on it. When it's ready, I'll remove the tag and update this PR.