zio / zio

ZIO — A type-safe, composable library for async and concurrent programming in Scala
https://zio.dev
Apache License 2.0
4.08k stars 1.28k forks source link

Subtle binary incompatibility that was not caught by mima #9160

Closed mschuwalow closed 1 month ago

mschuwalow commented 1 month ago

We just had a very interesting binary compat issue. Note that while the involved libraries are zio and zio-streams, the issue shows up in public interfaces so other projects might be affected as well.

The involved versions are as follows: zio: 2.1.9 zio-streams: 2.1.7

Running code with this setup fails with the following error:

java.lang.NoSuchMethodError: 'zio.ZIO zio.ZIO$.$anonfun$uninterruptible$1(scala.Function0, java.lang.Object)'
at zio.stream.internal.ChannelExecutor.runBracketOut(ChannelExecutor.scala:438)
at zio.stream.internal.ChannelExecutor.run(ChannelExecutor.scala:234)
at zio.stream.internal.ChannelExecutor$.read$1(ChannelExecutor.scala:730)
at zio.stream.internal.ChannelExecutor$.$anonfun$readUpstream$10(ChannelExecutor.scala:771)
at zio.ZIO.$anonfun$$times$greater$1(ZIO.scala:89)

Digging into the issue, ZIO.uninterruptible changed the following way between the two versions:

2.1.7:

def uninterruptible[R, E, A](zio: => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
  ZIO.suspendSucceed(zio.uninterruptible)

2.1.9:

def uninterruptible[R, E, A](zio: => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
  ZIO.suspendSucceed(zio).uninterruptible

This seems innocent enough and should be a binary compatible change, but what is actually getting called by zio-streams is this synthetic method (2.1.7):

public static final zio.ZIO $anonfun$uninterruptible$1(scala.Function0, java.lang.Object);
  descriptor: (Lscala/Function0;Ljava/lang/Object;)Lzio/ZIO;
  flags: (0x1019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
  Code:
    stack=2, locals=2, args_size=2
       0: aload_0
       1: invokeinterface #1889,  1         // InterfaceMethod scala/Function0.apply:()Ljava/lang/Object;
       6: checkcast     #179                // class zio/ZIO
       9: aload_1
      10: invokeinterface #6239,  2         // InterfaceMethod zio/ZIO.uninterruptible:(Ljava/lang/Object;)Lzio/ZIO;
      15: areturn
    LineNumberTable:
      line 4906: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      16     0 zio$10   Lscala/Function0;
          0      16     1 trace$198   Ljava/lang/Object;
  MethodParameters:
    Name                           Flags
    zio$10                         final
    trace$198                      final

This method does not exist on 2.1.9. I would have expected mima to catch this, but seems it either does not or we have misconfigured it here. IMO this is a very worrying issue, as this happens on a public interface effectively undermining our backwards compatibility guarantees.


joroKr21 commented 1 month ago

This can happen when inlining: https://github.com/zio/zio/blob/96d948af08897f4b853286e512da115272100a22/project/BuildHelper.scala#L54

mschuwalow commented 1 month ago

Ah, so you are saying the only reason that zio-streams is depending on that synthetic method is due to inlining and it wouldn't be there if compiled with inlining disabled?

That would be awesome, as that means that this issue is purely related to zio-streams (and an error on our side having different versions of zio and zio-streams) and can not show up in other projects.

mschuwalow commented 1 month ago

Just tested it, indeed the synthetic method is not used when inlining is disabled. That means that this problem can only occur due to version mismatches between zio and zio-streams, not external libs.

Thanks for the help @joroKr21, closing the issue.