Menu

Kotlin’s Assert Is Not Like Java’s Assert

OkHttp uses synchronized as an allocation free mutex. Our concurrency model is tricky enough that we’ve documented the rules! And if we forget the rules, we also use runtime assertions to catch mistakes:

  @Override void flush() {
    // Make sure we don’t hold a lock while doing I/O!
    assert (!Thread.holdsLock(Http2Stream.this));
    ...
  }

When we migrated to Kotlin in 4.0, our assertions migrated too:

  override fun flush() {
    // Make sure we don’t hold a lock while doing I/O!
    assert(!Thread.holdsLock(this@Http2Stream))
    ...
  }

This seems like a boring change but unfortunately it wasn’t! One of our users reported a performance regression introduced with the Kotlin conversion.

To better understand the regression, let’s look at how the Java assert keyword works. The Java code above is equivalent to this source code:

  @Override void flush() {
    if (Http2Stream.class.desiredAssertionStatus()) {
      if (!Thread.holdsLock(Http2Stream.this) == false) {
        throw new AssertionError();
      }
    }
    ...
  }

It first checks to see if assertions are enabled. (You can enable them with the -ea flag to the Java process.) Only if they are does it execute the potentially-expensive assert expression. And if that is false then our program crashes with an AssertionError.

The Kotlin assert keyword works differently:

  override fun flush() {
    if (!Thread.holdsLock(this@Http2Stream) == false) {
      if (Http2Stream::class.java.desiredAssertionStatus()) {
        throw AssertionError()
      }
    }
    ...
  }

Kotlin always executes the assert expression. Then it uses the -ea flag to decide whether to throw an AssertionError.

In OkHttp the cost of the Thread.holdsLock() call was too slow to run all the time. Our mitigation was to create our own assertion-like class that defers evaluation.

If you’re migrating a performance-sensitive codebase from Java to Kotlin, look out for assert!