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

Handling serialization of Default Proto instance #565

Merged
merged 6 commits into from
Jun 4, 2021

Conversation

nikhilsu
Copy link
Contributor

A Default instance of a Protobuf class serializes to an empty byte array. Hence, an exception is raised when trying to read from a stream with length = 0. Example stacktrace(from Red-Green TDD):

Could not create class com.twitter.chill.protobuf.TestMessages$FatigueCount
java.lang.RuntimeException: Could not create class com.twitter.chill.protobuf.TestMessages$FatigueCount
	at com.twitter.chill.protobuf.ProtobufSerializer.read(ProtobufSerializer.java:76)
	at com.twitter.chill.protobuf.ProtobufSerializer.read(ProtobufSerializer.java:40)
	at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:709)
	at com.twitter.chill.SerDeState.readObject(SerDeState.java:58)
	at com.twitter.chill.KryoPool.fromBytes(KryoPool.java:105)
	at com.twitter.chill.KryoPool.deepCopy(KryoPool.java:87)
	at com.twitter.chill.protobuf.ProtobufTest$$anonfun$2.apply(ProtobufTest.scala:69)
	at com.twitter.chill.protobuf.ProtobufTest$$anonfun$2.apply(ProtobufTest.scala:66)
	at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
	at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
	at org.scalatest.Transformer.apply(Transformer.scala:22)
	at org.scalatest.Transformer.apply(Transformer.scala:20)
	at org.scalatest.wordspec.AnyWordSpecLike$$anon$1.apply(AnyWordSpecLike.scala:1227)
	at org.scalatest.TestSuite$class.withFixture(TestSuite.scala:196)
	at org.scalatest.wordspec.AnyWordSpec.withFixture(AnyWordSpec.scala:1879)
	at org.scalatest.wordspec.AnyWordSpecLike$class.invokeWithFixture$1(AnyWordSpecLike.scala:1224)
	at org.scalatest.wordspec.AnyWordSpecLike$$anonfun$runTest$1.apply(AnyWordSpecLike.scala:1237)
	at org.scalatest.wordspec.AnyWordSpecLike$$anonfun$runTest$1.apply(AnyWordSpecLike.scala:1237)
	at org.scalatest.SuperEngine.runTestImpl(Engine.scala:306)
	at org.scalatest.wordspec.AnyWordSpecLike$class.runTest(AnyWordSpecLike.scala:1237)
	at org.scalatest.wordspec.AnyWordSpec.runTest(AnyWordSpec.scala:1879)
	at org.scalatest.wordspec.AnyWordSpecLike$$anonfun$runTests$1.apply(AnyWordSpecLike.scala:1296)
	at org.scalatest.wordspec.AnyWordSpecLike$$anonfun$runTests$1.apply(AnyWordSpecLike.scala:1296)
	at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:413)
	at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:401)
	at scala.collection.immutable.List.foreach(List.scala:392)
	at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:401)
	at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:396)
	at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:475)
	at org.scalatest.wordspec.AnyWordSpecLike$class.runTests(AnyWordSpecLike.scala:1296)
	at org.scalatest.wordspec.AnyWordSpec.runTests(AnyWordSpec.scala:1879)
	at org.scalatest.Suite$class.run(Suite.scala:1112)
	at org.scalatest.wordspec.AnyWordSpec.org$scalatest$wordspec$AnyWordSpecLike$$super$run(AnyWordSpec.scala:1879)
	at org.scalatest.wordspec.AnyWordSpecLike$$anonfun$run$1.apply(AnyWordSpecLike.scala:1341)
	at org.scalatest.wordspec.AnyWordSpecLike$$anonfun$run$1.apply(AnyWordSpecLike.scala:1341)
	at org.scalatest.SuperEngine.runImpl(Engine.scala:535)
	at org.scalatest.wordspec.AnyWordSpecLike$class.run(AnyWordSpecLike.scala:1341)
	at org.scalatest.wordspec.AnyWordSpec.run(AnyWordSpec.scala:1879)
	at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:45)
	at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1322)
	at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1316)
	at scala.collection.immutable.List.foreach(List.scala:392)
	at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:1316)
	at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:972)
	at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:971)
	at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:1482)
	at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:971)
	at org.scalatest.tools.Runner$.run(Runner.scala:798)
	at org.scalatest.tools.Runner.run(Runner.scala)
	at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.runScalaTest2or3(ScalaTestRunner.java:41)
	at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.main(ScalaTestRunner.java:28)
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.twitter.chill.protobuf.ProtobufSerializer.read(ProtobufSerializer.java:74)
	... 50 more
Caused by: com.google.protobuf.InvalidProtocolBufferException: Message missing required fields: target_id, suggested_id, serve_count
	at com.google.protobuf.UninitializedMessageException.asInvalidProtocolBufferException(UninitializedMessageException.java:81)
	at com.twitter.chill.protobuf.TestMessages$FatigueCount$Builder.buildParsed(TestMessages.java:324)
	at com.twitter.chill.protobuf.TestMessages$FatigueCount$Builder.access$200(TestMessages.java:271)
	at com.twitter.chill.protobuf.TestMessages$FatigueCount.parseFrom(TestMessages.java:211)
	... 55 more

@CLAassistant
Copy link

CLAassistant commented May 31, 2021

CLA assistant check
All committers have signed the CLA.

@nikhilsu nikhilsu force-pushed the nikhilsu/fix-proto-ser-edges branch from 0b3a6c3 to e0c60d6 Compare May 31, 2021 06:01
Copy link
Collaborator

@regadas regadas left a comment

Choose a reason for hiding this comment

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

Thanks! @nikhilsu Can you run scalafmtAll

@nikhilsu
Copy link
Contributor Author

nikhilsu commented Jun 1, 2021

@regadas thanks for the review. Just ran scalafmtAll.

@@ -69,6 +69,9 @@ public void write(Kryo kryo, Output output, Message mes) {
public Message read(Kryo kryo, Input input, Class<Message> pbClass) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@regadas Wanted to get your thoughts on handling null in the serializer.

I was thinking maybe we could serialize nulls to a magic number (-1) which would look like:

@Override
public void write(Kryo kryo, Output output, Message mes) {
    if (mes == null) {
        output.writeInt(-1, true);
    }
    ...
}

and

@Override
public Message read(Kryo kryo, Input input, Class<Message> pbClass) {
    try {      
        int size = input.readInt(true);
        if (size == -1) {
            return null;
        }
        ...
    } catch (Exception e) {
        throw new RuntimeException("Could not create " + pbClass, e); 
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

since kryo dispatches serialization based on the .getClass I think this code will never really be called on null in practice.

Can you show a test that fails now just using Kyro not the serializer directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek you're right, I cannot get the test to fail inside the serializer with null. However, the com.esotericsoftware.kryo.Serializer base class has a acceptsNull field that's injected through the constructor. Hence, depending on how it's initialized, the serializer might get a null object?

@@ -69,6 +69,9 @@ public void write(Kryo kryo, Output output, Message mes) {
public Message read(Kryo kryo, Input input, Class<Message> pbClass) {
try {
int size = input.readInt(true);
if (size == 0) {
return (Message) pbClass.getMethod("getDefaultInstance").invoke(null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we actually need to use reflection here?

Should we cache the method in private variable so we don't need to make this call on each default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Yes, we do as the getDefaultInstance method, like the parseFrom method is not defined on the Message abstraction.
  2. Good point. Might be better to cache it as well.

Copy link
Collaborator

@regadas regadas Jun 3, 2021

Choose a reason for hiding this comment

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

Good point on caching! reflection is expensive.

One thing about though is that this inability to parseFrom empty byte array only happens with old versions of protoc. I believe the proposed check is still useful in case somebody uses older versions.

Here we use a very old version so #571 updates it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with the cache changed.

@regadas thanks for #571

@codecov-commenter
Copy link

codecov-commenter commented Jun 2, 2021

Codecov Report

Merging #565 (8161cf0) into develop (e067ea8) will not change coverage.
The diff coverage is n/a.

❗ Current head 8161cf0 differs from pull request most recent head cba34e0. Consider uploading reports for the commit cba34e0 to get more accurate results
Impacted file tree graph

@@           Coverage Diff            @@
##           develop     #565   +/-   ##
========================================
  Coverage    91.69%   91.69%           
========================================
  Files           42       42           
  Lines         1397     1397           
  Branches        36       32    -4     
========================================
  Hits          1281     1281           
  Misses         116      116           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e067ea8...cba34e0. Read the comment docs.

@johnynek
Copy link
Collaborator

johnynek commented Jun 3, 2021

looks like the test fails.

Does it pass for you locally?

@nikhilsu
Copy link
Contributor Author

nikhilsu commented Jun 3, 2021

looks like the test fails.

Does it pass for you locally?

@johnynek sorry the tests caught a silly mistake I'd made. They should pass now - ran them locally. (Red-Green-Refactor for the win!)

@@ -43,21 +43,30 @@
* classes in play, which should not be very large.
* We can replace with a LRU if we start to see any issues.
*/
final protected HashMap<Class, Method> methodCache = new HashMap<Class, Method>();
final protected HashMap<Class, Method> parseMethodCache = new HashMap<Class, Method>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we keep the name methodCache to not change binary compatibility? We can just comment that this is the parseMethod?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek Good point. Done!

}
return meth;
}

protected Method getParse(Class cls) throws Exception {
return getMethodFromCache(cls, methodCache, "parseFrom", new Class[]{byte[].class});
Copy link
Collaborator

Choose a reason for hiding this comment

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

one thing I just noticed: we unconditionally allocate this every call now, in the past we only hit if once (when the cache was empty).

Can we make a private variable to hold this and allocate it only one time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm, I could also do:

return getMethodFromCache(cls, methodCache, "parseFrom", byte[].class);

Copy link
Collaborator

Choose a reason for hiding this comment

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

lets do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@johnynek johnynek merged commit 93e4cba into twitter:develop Jun 4, 2021
@johnynek
Copy link
Collaborator

johnynek commented Jun 4, 2021

Thank you!

@nikhilsu nikhilsu deleted the nikhilsu/fix-proto-ser-edges branch June 4, 2021 01:11
@nikhilsu
Copy link
Contributor Author

nikhilsu commented Jun 4, 2021

Thank you @johnynek!

Do we know when the next release would be? This fixes an issue we are facing in our Flink Job.

@regadas
Copy link
Collaborator

regadas commented Jun 4, 2021

I think we will do one really soon! probably after #571

@nikhilsu
Copy link
Contributor Author

nikhilsu commented Jun 4, 2021

Oh cool. Will the release be announced somewhere? I'd want to be notified.

@regadas
Copy link
Collaborator

regadas commented Jun 4, 2021

@nikhilsu
Copy link
Contributor Author

nikhilsu commented Jun 4, 2021

Awesome! Thanks for the update.

@johnynek
Copy link
Collaborator

johnynek commented Jun 4, 2021

Thanks @regadas !

@regadas
Copy link
Collaborator

regadas commented Jun 5, 2021

Thank you! no problem 👍

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 this pull request may close these issues.

5 participants