Open david-obee opened 1 year ago
Thank you for reporting and for the detailed investigation & write-up, @david-obee!
Regarding modifying the codegen, we could infer [SuppressReferenceTracking]
for types which derive from Exception
as a special-case. This would result in them always being serialized in full. If there was a reference cycle, the application would crash during serialization time with a StackOverflowException, however (at least that is easier to debug). eg here: https://github.com/dotnet/orleans/blob/28256dba1681e7fafde850ecb10c20b25faaf720/src/Orleans.CodeGenerator/Model/SerializableTypeDescription.cs#L198
In ExceptionCodec
, we should throw if the ReadReference
return value on the deserialization path is not null
: https://github.com/dotnet/orleans/blob/28256dba1681e7fafde850ecb10c20b25faaf720/src/Orleans.Serialization/ISerializableSerializer/ExceptionCodec.cs#L293
Expanding the documentation is a good idea. We could add a section specifically about exception serialization with an example showing how to do it right and a call-out telling developers to avoid creating potential reference cycles.
Any help you can provide is appreciated.
Hi @ReubenBond, thanks for the comments above, apologies I'm returning to this so late! I've raised a PR implementing your two suggestions here: https://github.com/dotnet/orleans/pull/8698.
There's two parts. The first part I think is definitely worth having (throwing an Exception over silently returning null
). That's quite simple and just helpful.
The second part I think is debatable on if it's worth the change (or if I've implemented it the best way). I'll leave decision on whether to keep it or not to you on the PR, I'm happy to take that second part out if you want.
Hi all, this issue is less of a real 'bug' in Orleans, as you can argue we're using it wrong here. However, it's part of a broader consideration about throwing exceptions from grains, with respect to serialization. I'm looking for some advice and to determine if it's worth expanding the documentation at all.
We've just had an issue caused because we used the
GenerateSerializerAttribute
to generate a codec for one of our custom exceptions, and that same exception object was referenced in two places. TheExceptionCodec
explicitly has special handling around this for serialization and deserialization:Serialization:
Deserialization:
The type we were serializing was like:
The issue comes when the exception contained in
BatchTransactionException.Results
on aResult
is our custom exception (although it can be any exception for which serialization code has been generated).On serialization, when serializing the exception, first the exception is serialized using it's own generated serializer as part of the
InnerException
on theBatchTransactionException
. As this is using the generated code, it serializes like any other type, and puts in a reference to the object in the serialization context.Then when it comes to serialize that exception again as part of the
Result
s, it is again using this code-gen serializer, so like any normal type, it recognises that the same object is already serialized (from the serialization context), so inserts a reference to it, rather than reserializing it from scratch.This is fine, until we come to deserialization. We get to deserializing the second instance of the
MyException
, the one in theResult
, which has been serialized as a reference to the other place it appears which is in theBatchTransactionException.InnerException
. However, at this point the deserializer only knows to expect some kind ofException
, so it loads up theExceptionCodec
, and uses that to deserialze. This then sees that we have serialized a reference, which we don't allow forException
s, so it get's serialized as anull
. This is completely silent, and hence difficult to spot why this breaks when you've not seen this issue before.To clarify, the reason we've gone with using the
GenerateSerializerAttribute
for these exceptions is because we need to serialze them fully. We're aware that we can configure theExceptionCodec
to run for our own exceptions by adjusting the namespaces it's considered for, however theExceptionCodec
is not perfect in that it does not handle extra custom properties. In this case, we have a custom exception that is part of our domain, so we want to be able to serialize it fully like we do with any other type our grain calls can return.I have a few suggestions for what could be done here. We could:
ExceptionCodec
)? For what it's worth in our case we have changed things so that we no longer do this to side-step the issue entirely.ExceptionCodec
works and how you can enable it for more types. I've seen this come up a couple of times in Discord, so I think it's worth having some guidance in the docs. Exceptions are different to other types, in that you can't tell from a signature all the possible exceptions a method could throw, and they can come from third party libraries, so I think there are special considerations that have to be made for them.Exception
derived types, so that they also get the same behavior with respect to references as theExceptionCodec
has. This would allow users to use the code-gen for exceptions and not hit any issues with theExceptionCodec
.If you agree with any of these suggestions, I would be happy to help out if I can.