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

Do we need a thread-safe alternative to ModuleScope.DefineType? #399

Closed
stakx opened this issue Jun 25, 2018 · 19 comments · Fixed by #436
Closed

Do we need a thread-safe alternative to ModuleScope.DefineType? #399

stakx opened this issue Jun 25, 2018 · 19 comments · Fixed by #436
Milestone

Comments

@stakx
Copy link
Member

stakx commented Jun 25, 2018

@zvirja alerted me to the fact that after deprecating Lock (in #391), it seems no longer possible in the layers above DynamicProxy to use ModuleScope.DefineType in a thread-safe manner; see https://github.com/nsubstitute/NSubstitute/pull/420/files#r197765187.

I see three options to resolve this:

  1. Do nothing.
    That is, leave it up to downstream libraries to ensure that if they access ModuleScope's ModuleBuilders, they must not call into their ProxyGenerator at the same time.

  2. Deprecate ModuleScope.DefineType as well.
    Not very realistic because, for instance, NSubstitute and Moq both rely on this API in order to mock delegate types.

  3. Revert Deprecate Lock and ModuleScope's public type cache API #391.
    I'm biased here, but I think it would be a pity because that PR went in the right direction of closing doors into DynamicProxy's internals.

  4. Expose the ReaderWriterLockSlim lock that ModuleScope uses internally.
    ModuleScope has an API that exposes the two ModuleBuilders that it uses, which essentially means that once a user starts accessing ModuleBuilders, all thread-safety guarantees are off. We can no longer expose a Lock, but we could expose the internal ReaderWriterLockSlim instead.

  5. Augment ModuleScope with a thread-safe alternative for DefineType.
    Apart from (0) above, this is my personal preference. This could be in the form of a new method on ModuleScope which performs the necessary locking internally, for example:

    +[Obsolete("This method is not thread-safe. Please use `ModuleBuilder.CreateType` instead.")]
     public TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags)
     {
         …
     }
    
    +public Type CreateType(bool inStrongNamedModulePreferably,
    +                       string name,
    +                       TypeAttributes flags,
    +                       Func<TypeBuilder, Type> build)
    +{
    +    TypeCache.Lock.EnterWriteLock();
    +    try
    +    {
    +        var module = ObtainDynamicModule(disableSignedModule == false && inStrongNamedModulePreferably);
    +        var typeBuilder = module.DefineType(name, flags);
    +        return build.Invoke(typeBuilder);
    +    }
    +    finally
    +    {
    +        TypeCache.Lock.ExitWriteLock();
    +    }
    +}

    (This option would probably entail deprecating the ObtainDynamicModule API as well. ModuleScope would eventually have a less powerful API, but one where thread-safety would already be taken care of for downstream libraries.)

@castleproject/committers, do you see any other options here? If not, which one of the above do you think we should go for?

@zvirja
Copy link
Contributor

zvirja commented Jun 25, 2018

@stakx Thanks for raising the issue.

I'm in for the option 4, however I'd vote to shape the API in a bit different way:

public Type DefineTypeSynchronized(
    bool inStrongNamedModulePreferably,
    Func<ModuleBuilder, Type> builder)
{
    TypeCache.Lock.EnterWriteLock();
    try
    {
        var module = ObtainDynamicModule(disableSignedModule == false && inStrongNamedModulePreferably);
        return build.Invoke(module);
    }
    finally
    {
        TypeCache.Lock.ExitWriteLock();
    }
}

Such API will allow to perform the lookup before creating any type - you might want to look up for the already defined type and return it if it exists. That's exactly the scenario I met - I need to define System.Runtime.CompilerServices.IsReadOnlyAttribute type and do that only if it doesn't already exist. Otherwise, the exception about duplicate type is raised.

@stakx
Copy link
Member Author

stakx commented Jun 25, 2018

You might want to look up for the already defined type and return it if it exists.

Wouldn't it be more logical to use a Lazy<Type> at your end? It seems somewhat strange to give access to a whole module, but ask back for only a type.

(Btw. you can still access the module by querying a TypeBuilder's Module property, IIRC.)

@zvirja
Copy link
Contributor

zvirja commented Jun 25, 2018

One more thing to consider given that we are around. Currently you work with ModuleBuilder without any synchronization and rely that it's thread-safe (all synchronization is around cache only). However, in fact, this thread safety is not guaranteed and is implementation specific. Given that we target .NET Standard, I'd suggest to play safely.

Therefore, I'm both hands for the option 4 where each access to the ModuleBuilder is fully synchronized.

Wouldn't it be more logical to use a Lazy at your end?

Please provide a sample, not sure I got it 😟

(Btw. you can still access the module by querying a TypeBuilder's Module property, IIRC.)

The point is to test before creating a TypeBuilder. Otherwise, you'll get an exception and it isn't very suitable. If we consume the ModuleBuilder, we have an option to not define any type.

@stakx
Copy link
Member Author

stakx commented Jun 25, 2018

The point is to test before creating a TypeBuilder.

Ah, of course. Now I feel a little stupid. :-D

Granted, perhaps such a new method would have to deal with failure more gracefully. It could be converted to e.g. the Try or GetOrAdd pattern.

Please provide a sample, not sure I got it 😟

(See nsubstitute/NSubstitute#420 (comment).)

@zvirja
Copy link
Contributor

zvirja commented Jun 25, 2018

It could be converted to e.g. the Try or GetOrAdd pattern.

Probably it's a good idea. But we must be sure that introduced API enables all the potential usages, rather than a single dedicated scenario. The way with callback provided us with flexibility (e.g. perform any non-trivial look-ups), while GetOrAdd might offer only a limited usage scenario.

Wouldn't it be more logical to use a Lazy at your end? It seems somewhat strange to give access to a whole module, but ask back for only a type.

(See nsubstitute/NSubstitute#420 (comment).)

It might be hard to follow this issue if we need to constantly switch between this issue and original PR caused it 😅 Therefore, let's omit that part from here 😄

The goal is to provide API which allows to synchronize type creation between Castle.Core and any potential client. It might rather a callback or an exposed RWLock - doesn't matter too much. Let's see what other maintainers think about the shape of API.

@stakx stakx changed the title We need a thread-safe alternative to ModuleScope.DefineType Do we need a thread-safe alternative to ModuleScope.DefineType? Jun 25, 2018
@jonorossi
Copy link
Member

Sorry for the silence from my end. The current API allows a user to obtain the lock and write multiple types before releasing the lock, is this something the new API should also support?

@zvirja
Copy link
Contributor

zvirja commented Jul 12, 2018

@jonorossi Could please provide a snippet of code? I want to sync with Castle's code and my own code (so we all are taking the same lock) and am unable to find the appropriate API.

@jonorossi
Copy link
Member

Could please provide a snippet of code? I want to sync with Castle's code and my own code (so we all are taking the same lock) and am unable to find the appropriate API.

BaseProxyGenerator.ObtainProxyType synchronises each generator's "GenerateType" factory method using ModuleScope.TypeCache.GetOrAdd, ModuleScope.Lock is the same lock instance. Unless I'm misunderstanding the current code, this is the lock you'd take to be able to call ModuleScope.DefineType

ModuleScope.DefineType was probably never intended for external use even though it is marked public. I need to look at how the NSubstitute and Moq are actually using this for delegate types as I'm unsure why they need to emit their own types into DP's assembly and couldn't just use their own. If it is just a matter of convenience (i.e. not having to deal with internals visible to) then okay. Without me understanding how this is used externally I couldn't really comment on how the public API should look.

@zvirja
Copy link
Contributor

zvirja commented Jul 12, 2018

factory method using ModuleScope.TypeCache.GetOrAdd

This method doesn't use the ModuleScope.Lock as a sync object. In fact, the ModuleScope.Lock is never used in your codebase now, so we can't use it reliably. Additionally, the ModuleScope.Lock property is obsolete now.

I'm unsure why they need to emit their own types into DP's assembly and couldn't just use their own.

I wasn't thinking about that before, but yes, it could happen that user tries to create delegate with e.g. internal types for parameters. In this case he can use InternalsVisibleTo for the Castle.Core. But if we decide to use our own assembly, it will be a mess to work with all that stuff both for us and for client.

@jonorossi
Copy link
Member

This method doesn't use the ModuleScope.Lock as a sync object. In fact, the ModuleScope.Lock is never used in your codebase now, so we can't use it reliably. Additionally, the ModuleScope.Lock property is obsolete now.

ModuleScope.Lock returns the ModuleScope.cacheLock field which is created from the typeCache, it is the sole reason SynchronizedDictionary has a Lock property of type ReaderWriterLockSlim to expose its internal lock.

this.cacheLock = Lock.CreateFor(typeCache.Lock);

Yes, ModuleScope.Lock is marked obsolete but it won't be removed in the 4.x lineage so is still safe to use.

I wasn't thinking about that before, but yes, it could happen that user tries to create delegate with e.g. internal types for parameters. [...] But if we decide to use our own assembly, it will be a mess to work with all that stuff both for us and for client.

If both Moq and NSubstitute are doing the same thing for mocking delegates maybe this is something DP should support, quickly skimming the Moq codebase I don't yet understand why it needs to create an interface with an Invoke method matching the delegate's signature, if someone wants to explain it to me that would be great, otherwise I'll try to find some time to look at it.

With the amount of work the .NET teams are putting into AOT I can see we'll have scenarios to support in the near future where we can't have others generating random types into our DP assembly, otherwise we won't be able to load up the DP assembly and for things to just work. Moq's delegateInterfaceCache won't work if we loaded up the DP assembly because it wouldn't know about the existing types and would try creating new ones.

I'd like to see if we can do "1. Deprecate ModuleScope.DefineType as well".

@stakx
Copy link
Member Author

stakx commented Jul 12, 2018

skimming the Moq codebase I don't yet understand why it needs to create an interface with an Invoke method matching the delegate's signature, if someone wants to explain it to me that would be great,

I'll try to come up with a detailed explanation, right now I'm not too sure myself. I suspect it has to do with Moq being able to internally store delegate mocks the same way as regular class/interface mocks.

I'd like to see if we can do "1. Deprecate ModuleScope.DefineType as well".

I have a feeling this might be problematic for downstream libs, but regarding Moq, I'll do some brainstorming to see whether there's another way to mock delegates that might also work for NSubstitute et al.

If we cannot deprecate this, we could always use the callbsck approach we're using with GetOrAdd to hide the locking logic. This could either be done for defining types (DefineType(string typeName, Action<TypeBuilder>)) or even at the module level if downstream libs require that much freedom (DoWithStrongNamedModule(Action<ModuleBuilder>) — though I don't like the latter suggestion at all.

@jonorossi
Copy link
Member

@stakx thanks. I'm just brainstorming so I'm happy to see us unpick this and work out the best outcome once we know all the details. If we do have to go with the callback option then we should resolve the problem of Moq's delegate cache being broken if the assembly is saved and reloaded.

@zvirja
Copy link
Contributor

zvirja commented Jul 12, 2018

I don't yet understand why it needs to create an interface with an Invoke method matching the delegate's signature, if someone wants to explain it to me that would be great, otherwise I'll try to find some time to look at it.

I could explain why we do so for the NSubstitute. Previously to support delegate proxy we created the expression trees and compiled them. However, it has been reported that it was very slow:

Method Median
SubstituteForInterface 6.3367 us
SubstituteForClass 6.5393 us
SubstituteForDelegate 545.3938 us

I found a way to improve that:

  • emit a dynamic interface with the Invoke() method matching the delegate's signature
  • ask Castle.Core to create a proxy for that interface
  • create a delegate based on the returned proxy's Invoke() method.

It helped a lot:

Method Mean
SubstituteForInterface 5.165 us
SubstituteForClass 5.399 us
SubstituteForDelegate 10.312 us

Now delegate's proxy is only twice slower which is fine.

It appeared to be very simple, but effective approach. And it allowed to unify the proxy behavior, as we use the Castle in all the scenarios.

Moq's delegateInterfaceCache won't work if we loaded up the DP assembly because it wouldn't know about the existing types and would try creating new ones.

I'm not sure I got the point why you are going to load the DP assembly with the already constructed interface types - they are created at run time, so you don't know about them upfront... 😕 In any case, if the problem is only that we might start to create the duplicated types, it's enough to use the namespace with Guid, so the interface types will be always unique 😉 Will add that at NSubstitute part, it's a very good point!

@jonorossi
Copy link
Member

Thanks @zvirja, I see it is a recent performance improvement this year. Looks like a good solution, and likely something DP should do for you.

I'm not sure I got the point why you are going to load the DP assembly with the already constructed interface types - they are created at run time, so you don't know about them upfront... 😕

Back in the old days of partial trust web hosting it was a common request from NHibernate users to be able to use lazy loading which wasn't possible under partial trust because you couldn't emit types; web app startup performance was also the other reason. Support was added to save and load dynamically generated types via ModuleScope.SaveAssembly and ModuleScope.LoadAssemblyIntoCache which emits a special DP CacheMappingsAttribute assembly attribute so DP can load the types back into the cache. The idea is that DP would never hit the reflection emit codepath as all its types would be available in the cache, it was up to the developer to ensure they built this assembly with everything.

This save/load functionality isn't available in our .NET Standard builds because it uses binary serialisation and also has some limitations with certain usage of ProxyGenerationOptions, however what I was thinking is that we'll likely get similar feature requests to support DP on CoreRT. I think users are also likely to want mocking on CoreRT where they could run their unit tests on CoreCLR, save the dynamic assembly out (can't do this yet), then run their unit tests again on CoreRT to make sure things work on that runtime. I think the implementation of this feature needs to be improved when we go to add CoreCLR/CoreRT support.

In any case, if the problem is only that we might start to create the duplicated types, it's enough to use the namespace with Guid, so the interface types will be always unique 😉 Will add that at NSubstitute part, it's a very good point!

As you can see the problem isn't about duplicate types, you won't even get duplicate types because the scenario people would use this functionality is where refemit isn't available so you'd hit a PlatformNotSupportedException because it couldn't emit anything. If someone did this on .NET Framework NSubstitute should still be fine because the loaded assembly is not the dynamic assembly of the ModuleScope that new types get emitted into, only the types in the typeCache have metadata written out for them and then loaded back into the typeCache. This means Moq's delegateInterfaceCache is also fine on .NET Framework, but it too will start generating new types in the empty DynamicAssembly rather than using the ones it already has. If we made DP create these types then it could make sure these types participated in the save/load dance.

@zvirja
Copy link
Contributor

zvirja commented Jul 18, 2018

@jonorossi Sorry for the belated reply and many thanks for such a detailed explanation - now I clearly see the point! 👍 🍷

As for the module save/restore, I see value for libraries like NHibernate, which is used for the production code. However, for test libraries probably that's not such important... 😕

Anyway, currently we have 2 open questions to discuss:

  • the reliable way to sync type creation with Castle.Core starting from the next major.
  • ensure that ModuleScope.Lock is present and reliable till the next major.
  • delegate proxy support.

If we add delegates proxy support, NSubstitute will not need to use the emit anymore, so it would be a good feature. Likely, the Moq as well 😊

@stakx
Copy link
Member Author

stakx commented Aug 4, 2018

@jonorossi - As it turns out, adding a delegate proxying capability to DynamicProxy would be rather simple because the necessary bits already appear to be in place. (See PR #403.) It's mostly a question whether we actually want to do it, and what the API should look like, and how much of DynamicProxy we want to deprecate in the process.

@stakx
Copy link
Member Author

stakx commented Aug 4, 2018

Speaking for Moq 4: Assuming that ModuleScope.DefineType were to go away:

  • If there were a new ProxyGenerator.CreateDelegateProxy method, there would have to be some changes because Moq's delegate proxying is currently based on interface proxy types, while DynamicProxy would likely create a class proxy type. I haven't assessed yet how extensive the changes would be exactly, but it should be within the bounds of possibility.

  • If there were no ProxyGenerator.CreateDelegateProxy method, I would probably change Moq's implementation to either (a) use the already available DelegateProxyGenerator, or (b) emit delegate proxy types into separate dynamic assemblies on its own (instead of reusing DynamicProxy's), and make use of IgnoresAccessChecksToAttribute to get past the internals visibility problem (see above and Should DynamicProxy emit IgnoresAccessChecksToAttribute? #402). Consequences: (a) would require a small addition to DP's DelegateProxyGenerator (namely support for specifying additional interfaces to implement). (b) would mean that Moq could no longer target .NET 4.5, but would have to advance to .NET 4.6. That would be OK. (See e.g. Should Moq stop targeting .NET Standard in favor of .NET Core? devlooped/moq#630 (comment).) (@zvirja: Perhaps (a) or (b) might be an option for NSubstitute, too?)

So, from Moq's perspective, assuming that either (a) or (b) are feasible, deprecating ModuleScope.DefineType should be fine. I'm generally in favor of it. When it comes to a ProxyGenerator.CreateDelegateProxy, having it probably isn't crucial, but it would be greatly appreciated because it would save Moq from having to do its own low-level type emitting code (and it would be a more "official" API than using DelegateProxyGenerator directly).

@jonorossi
Copy link
Member

(b) emit delegate proxy types into separate dynamic assemblies on its own (instead of reusing DynamicProxy's), and make use of IgnoresAccessChecksToAttribute to get past the internals visibility problem (see above and #402). Consequences: [...] (b) would mean that Moq could no longer target .NET 4.5, but would have to advance to .NET 4.6. That would be OK.

@stakx I thought you discovered that emitting IgnoresAccessChecksToAttribute has to be done before types are emitted (as the runtime only looks for it once) meaning that Moq would need a dynamic assembly per assembly being mocked.

So, from Moq's perspective, assuming that either (a) or (b) are feasible, deprecating ModuleScope.DefineType should be fine. I'm generally in favor of it. When it comes to a ProxyGenerator.CreateDelegateProxy, having it probably isn't crucial, but it would be greatly appreciated because it would save Moq from having to do its own low-level type emitting code (and it would be a more "official" API than using DelegateProxyGenerator directly).

I'm definitely in favour of it, I don't like that others are emitting into the DP assembly, it would also mean that Windsor can use this more public API. I'll check out the pull request.

@stakx
Copy link
Member Author

stakx commented Aug 9, 2018

@jonorossi:

I thought you discovered that emitting IgnoresAccessChecksToAttribute has to be done before types are emitted (as the runtime only looks for it once) meaning that Moq would need a dynamic assembly per assembly being mocked.

Right. And that makes it unsuitable for DynamicProxy because it's all set up for at most two dynamic modules and downstream code probably expects this.

But in Moq, we don't have these constraints. Moq could produce as many dynamic assemblies as it wants and keep track of which ones have the visibility attribute(s) for which client assemblies. Like AArnott suggested, one would only generate a new dynamic assembly if none of the existing ones have suitable visibility attributes.

But creating such refined machinery just to support delegate proxies would be total overkill IMHO, I hope it won't be necessary. 😁 It might be an interesting pattern to keep in mind for DP vNextNext, perhaps.

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 a pull request may close this issue.

3 participants