diff --git a/akka-coordination/src/main/mima-filters/2.6.4.backwards.excludes b/akka-coordination/src/main/mima-filters/2.6.4.backwards.excludes new file mode 100644 index 0000000000..37d0752dd7 --- /dev/null +++ b/akka-coordination/src/main/mima-filters/2.6.4.backwards.excludes @@ -0,0 +1,2 @@ +# Wasn't meant for user extension +ProblemFilters.exclude[FinalClassProblem]("akka.coordination.lease.scaladsl.LeaseProvider") \ No newline at end of file diff --git a/akka-coordination/src/main/scala/akka/coordination/lease/internal/LeaseAdapter.scala b/akka-coordination/src/main/scala/akka/coordination/lease/internal/LeaseAdapter.scala index 9f8e9f14de..2d4f4a7df1 100644 --- a/akka-coordination/src/main/scala/akka/coordination/lease/internal/LeaseAdapter.scala +++ b/akka-coordination/src/main/scala/akka/coordination/lease/internal/LeaseAdapter.scala @@ -10,18 +10,19 @@ import java.util.function.Consumer import akka.annotation.InternalApi import akka.coordination.lease.LeaseSettings -import akka.coordination.lease.javadsl.Lease import akka.coordination.lease.scaladsl.{ Lease => ScalaLease } +import akka.coordination.lease.javadsl.{ Lease => JavaLease } import scala.compat.java8.FutureConverters._ import scala.compat.java8.OptionConverters._ import scala.concurrent.ExecutionContext +import scala.concurrent.Future /** * INTERNAL API */ @InternalApi -final private[akka] class LeaseAdapter(delegate: ScalaLease)(implicit val ec: ExecutionContext) extends Lease { +final private[akka] class LeaseAdapter(delegate: ScalaLease)(implicit val ec: ExecutionContext) extends JavaLease { override def acquire(): CompletionStage[java.lang.Boolean] = delegate.acquire().map(Boolean.box).toJava @@ -33,3 +34,23 @@ final private[akka] class LeaseAdapter(delegate: ScalaLease)(implicit val ec: Ex override def checkLease(): Boolean = delegate.checkLease() override def getSettings(): LeaseSettings = delegate.settings } + +/** + * INTERNAL API + */ +@InternalApi +final private[akka] class LeaseAdapterToScala(val delegate: JavaLease)(implicit val ec: ExecutionContext) + extends ScalaLease(delegate.getSettings()) { + + override def acquire(): Future[Boolean] = + delegate.acquire().toScala.map(Boolean.unbox) + + override def acquire(leaseLostCallback: Option[Throwable] => Unit): Future[Boolean] = + delegate.acquire(o => leaseLostCallback(o.asScala)).toScala.map(Boolean.unbox) + + override def release(): Future[Boolean] = + delegate.release().toScala.map(Boolean.unbox) + + override def checkLease(): Boolean = + delegate.checkLease() +} diff --git a/akka-coordination/src/main/scala/akka/coordination/lease/javadsl/LeaseProvider.scala b/akka-coordination/src/main/scala/akka/coordination/lease/javadsl/LeaseProvider.scala index 82428965e7..d774d00cf6 100644 --- a/akka-coordination/src/main/scala/akka/coordination/lease/javadsl/LeaseProvider.scala +++ b/akka-coordination/src/main/scala/akka/coordination/lease/javadsl/LeaseProvider.scala @@ -7,6 +7,7 @@ package akka.coordination.lease.javadsl import akka.actor.ClassicActorSystemProvider import akka.actor.{ ActorSystem, ExtendedActorSystem, Extension, ExtensionId, ExtensionIdProvider } import akka.coordination.lease.internal.LeaseAdapter +import akka.coordination.lease.internal.LeaseAdapterToScala import akka.coordination.lease.scaladsl.{ LeaseProvider => ScalaLeaseProvider } object LeaseProvider extends ExtensionId[LeaseProvider] with ExtensionIdProvider { @@ -35,6 +36,10 @@ class LeaseProvider(system: ExtendedActorSystem) extends Extension { */ def getLease(leaseName: String, configPath: String, ownerName: String): Lease = { val scalaLease = delegate.getLease(leaseName, configPath, ownerName) - new LeaseAdapter(scalaLease)(system.dispatchers.internalDispatcher) + // unwrap if this is a java implementation + scalaLease match { + case adapter: LeaseAdapterToScala => adapter.delegate + case _ => new LeaseAdapter(scalaLease)(system.dispatchers.internalDispatcher) + } } } diff --git a/akka-coordination/src/main/scala/akka/coordination/lease/scaladsl/LeaseProvider.scala b/akka-coordination/src/main/scala/akka/coordination/lease/scaladsl/LeaseProvider.scala index 33740e82a9..d9de061bec 100644 --- a/akka-coordination/src/main/scala/akka/coordination/lease/scaladsl/LeaseProvider.scala +++ b/akka-coordination/src/main/scala/akka/coordination/lease/scaladsl/LeaseProvider.scala @@ -17,6 +17,9 @@ import akka.actor.ExtensionId import akka.actor.ExtensionIdProvider import akka.event.Logging import akka.coordination.lease.LeaseSettings +import akka.coordination.lease.internal.LeaseAdapterToScala + +import scala.reflect.ClassTag object LeaseProvider extends ExtensionId[LeaseProvider] with ExtensionIdProvider { override def get(system: ActorSystem): LeaseProvider = super.get(system) @@ -29,7 +32,7 @@ object LeaseProvider extends ExtensionId[LeaseProvider] with ExtensionIdProvider private final case class LeaseKey(leaseName: String, configPath: String, clientName: String) } -class LeaseProvider(system: ExtendedActorSystem) extends Extension { +final class LeaseProvider(system: ExtendedActorSystem) extends Extension { import LeaseProvider.LeaseKey private val log = Logging(system, getClass) @@ -46,6 +49,10 @@ class LeaseProvider(system: ExtendedActorSystem) extends Extension { * @param ownerName the owner that will `acquire` the lease, e.g. hostname and port of the ActorSystem */ def getLease(leaseName: String, configPath: String, ownerName: String): Lease = { + internalGetLease(leaseName, configPath, ownerName) + } + + private[akka] def internalGetLease(leaseName: String, configPath: String, ownerName: String): Lease = { val leaseKey = LeaseKey(leaseName, configPath, ownerName) leases.computeIfAbsent( leaseKey, @@ -54,39 +61,54 @@ class LeaseProvider(system: ExtendedActorSystem) extends Extension { val leaseConfig = system.settings.config .getConfig(configPath) .withFallback(system.settings.config.getConfig("akka.coordination.lease")) - loadLease(LeaseSettings(leaseConfig, leaseName, ownerName), configPath) + + val settings = LeaseSettings(leaseConfig, leaseName, ownerName) + + // Try and load a scala implementation + val lease: Try[Lease] = + loadLease[Lease](settings).recoverWith { + case _: ClassCastException => + // Try and load a java implementation + loadLease[akka.coordination.lease.javadsl.Lease](settings).map(javaLease => + new LeaseAdapterToScala(javaLease)(system.dispatchers.internalDispatcher)) + } + + lease match { + case Success(value) => value + case Failure(e) => + log.error( + e, + "Invalid lease configuration for leaseName [{}], configPath [{}] lease-class [{}]. " + + "The class must implement scaladsl.Lease or javadsl.Lease and have constructor with LeaseSettings parameter and " + + "optionally ActorSystem parameter.", + settings.leaseName, + configPath, + settings.leaseConfig.getString("lease-class")) + throw e + } } }) } - private def loadLease(leaseSettings: LeaseSettings, configPath: String): Lease = { + /** + * The Lease types are separate for Java and Scala and A java lease needs to be loadable + * from Scala and vice versa as leases can be in libraries and user should not care what + * language it is implemented in. + */ + private def loadLease[T: ClassTag](leaseSettings: LeaseSettings): Try[T] = { val fqcn = leaseSettings.leaseConfig.getString("lease-class") require(fqcn.nonEmpty, "lease-class must not be empty") val dynamicAccess = system.dynamicAccess - val instance: Try[Lease] = dynamicAccess.createInstanceFor[Lease]( + dynamicAccess.createInstanceFor[T]( fqcn, immutable.Seq((classOf[LeaseSettings], leaseSettings), (classOf[ExtendedActorSystem], system))) match { - case s: Success[Lease] => + case s: Success[T] => s case Failure(_: NoSuchMethodException) => - dynamicAccess.createInstanceFor[Lease](fqcn, immutable.Seq((classOf[LeaseSettings], leaseSettings))) + dynamicAccess.createInstanceFor[T](fqcn, immutable.Seq((classOf[LeaseSettings], leaseSettings))) case f: Failure[_] => f } - instance match { - case Success(value) => value - case Failure(e) => - log.error( - e, - "Invalid lease configuration for leaseName [{}], configPath [{}] lease-class [{}]. " + - "The class must implement Lease and have constructor with LeaseSettings parameter and " + - "optionally ActorSystem parameter.", - leaseSettings.leaseName, - configPath, - fqcn) - throw e - } - } - // TODO how to clean up a lease? Not important for this use case as we'll only have one lease + } } diff --git a/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java b/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java index cae6d5a7d5..c1c2d53b22 100644 --- a/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java +++ b/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java @@ -5,7 +5,6 @@ package jdocs.akka.coordination.lease; import akka.actor.ActorSystem; -import akka.cluster.Cluster; import akka.coordination.lease.LeaseSettings; import akka.coordination.lease.javadsl.Lease; import akka.coordination.lease.javadsl.LeaseProvider; @@ -14,13 +13,15 @@ import docs.akka.coordination.LeaseDocSpec; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.scalatestplus.junit.JUnitSuite; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; @SuppressWarnings("unused") -public class LeaseDocTest { +public class LeaseDocTest extends JUnitSuite { // #lease-example static class SampleLease extends Lease { @@ -37,22 +38,22 @@ public class LeaseDocTest { @Override public CompletionStage acquire() { - return null; + return CompletableFuture.completedFuture(true); } @Override public CompletionStage acquire(Consumer> leaseLostCallback) { - return null; + return CompletableFuture.completedFuture(true); } @Override public CompletionStage release() { - return null; + return CompletableFuture.completedFuture(true); } @Override public boolean checkLease() { - return false; + return true; } } // #lease-example @@ -73,10 +74,10 @@ public class LeaseDocTest { private void doSomethingImportant(Optional leaseLostReason) {} @Test - public void beLoadable() { + public void javaLeaseBeLoadableFromJava() { // #lease-usage Lease lease = - LeaseProvider.get(system).getLease("", "docs-lease", ""); + LeaseProvider.get(system).getLease("", "jdocs-lease", ""); CompletionStage acquired = lease.acquire(); boolean stillAcquired = lease.checkLease(); CompletionStage released = lease.release(); @@ -87,8 +88,18 @@ public class LeaseDocTest { // #lost-callback // #cluster-owner - String owner = Cluster.get(system).selfAddress().hostPort(); + // String owner = Cluster.get(system).selfAddress().hostPort(); // #cluster-owner } + + @Test + public void scalaLeaseBeLoadableFromJava() { + Lease lease = + LeaseProvider.get(system).getLease("", "docs-lease", ""); + CompletionStage acquired = lease.acquire(); + boolean stillAcquired = lease.checkLease(); + CompletionStage released = lease.release(); + lease.acquire(this::doSomethingImportant); + } } diff --git a/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala b/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala index 30c14ff98f..4e6fa168f4 100644 --- a/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala +++ b/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala @@ -35,7 +35,9 @@ class SampleLease(settings: LeaseSettings) extends Lease(settings) { object LeaseDocSpec { - val config = ConfigFactory.parseString(""" + def config() = + ConfigFactory.parseString(""" + jdocs-lease.lease-class = "jdocs.akka.coordination.lease.LeaseDocTest$SampleLease" #lease-config akka.actor.provider = cluster docs-lease { @@ -62,7 +64,7 @@ class LeaseDocSpec extends AkkaSpec(LeaseDocSpec.config) { import LeaseDocSpec._ "A docs lease" should { - "be loadable" in { + "scala lease be loadable from scala" in { //#lease-usage val lease = LeaseProvider(system).getLease("", "docs-lease", "owner") @@ -83,6 +85,16 @@ class LeaseDocSpec extends AkkaSpec(LeaseDocSpec.config) { blackhole(acquired, stillAcquired, released, owner) } + + "java lease be loadable from scala" in { + val lease = LeaseProvider(system).getLease("", "jdocs-lease", "owner") + val acquired: Future[Boolean] = lease.acquire() + val stillAcquired: Boolean = lease.checkLease() + val released: Future[Boolean] = lease.release() + lease.acquire(leaseLostReason => doSomethingImportant(leaseLostReason)) + + blackhole(acquired, stillAcquired, released) + } } }