diff --git a/akka-bench-jmh/src/main/scala/akka/http/HttpMessageMatchingBenchmark.scala b/akka-bench-jmh/src/main/scala/akka/http/HttpMessageMatchingBenchmark.scala new file mode 100644 index 0000000000..a1a6e94619 --- /dev/null +++ b/akka-bench-jmh/src/main/scala/akka/http/HttpMessageMatchingBenchmark.scala @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2015-2016 Lightbend Inc. + */ + +package akka.http + +import akka.http.scaladsl.model._ +import java.util.concurrent.TimeUnit +import org.openjdk.jmh.annotations._ + +/** + * Benchmark used to verify move to name-based extraction does not hurt preformance. + * It does not allocate an Option thus it should be more optimal actually. + */ +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Array(Mode.Throughput)) +class HttpMessageMatchingBenchmark { + + val req = HttpRequest() + val res = HttpResponse() + + @Benchmark + def res_matching: HttpResponse = { + res match { + case r @ HttpResponse(status, headers, entity, protocol) => r + } + } + + @Benchmark + def req_matching: HttpRequest = { + req match { + case r @ HttpRequest(method, uri, headers, entity, protocol) => r + } + } + +} \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala index 82660cdf6b..e1e8cbda91 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala @@ -13,7 +13,7 @@ import scala.collection.immutable import scala.reflect.{ classTag, ClassTag } import akka.parboiled2.CharUtils import akka.stream.Materializer -import akka.util.ByteString +import akka.util.{HashCode, ByteString} import akka.http.impl.util._ import akka.http.javadsl.{ model ⇒ jm } import akka.http.scaladsl.util.FastFuture._ @@ -134,11 +134,14 @@ object HttpMessage { /** * The immutable model HTTP request model. */ -final case class HttpRequest(method: HttpMethod = HttpMethods.GET, - uri: Uri = Uri./, - headers: immutable.Seq[HttpHeader] = Nil, - entity: RequestEntity = HttpEntity.Empty, - protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`) extends jm.HttpRequest with HttpMessage { +final class HttpRequest( + val method: HttpMethod, + val uri: Uri, + val headers: immutable.Seq[HttpHeader], + val entity: RequestEntity, + val protocol: HttpProtocol) + extends jm.HttpRequest with HttpMessage { + HttpRequest.verifyUri(uri) require(entity.isKnownEmpty || method.isEntityAccepted, s"Requests with method '${method.value}' must have an empty entity") require(protocol != HttpProtocols.`HTTP/1.0` || !entity.isInstanceOf[HttpEntity.Chunked], @@ -196,6 +199,46 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET, override def getUri: jm.Uri = uri.asJava /** Java API */ override def withUri(uri: jm.Uri): HttpRequest = copy(uri = uri.asScala) + + /* Manual Case Class things, to easen bin-compat */ + + def copy(method: HttpMethod = method, + uri: Uri = uri, + headers: immutable.Seq[HttpHeader] = headers, + entity: RequestEntity = entity, + protocol: HttpProtocol = protocol) = new HttpRequest(method, uri, headers, entity, protocol) + + + + override def hashCode(): Int = { + var result = HashCode.SEED + result = HashCode.hash(result, _1) + result = HashCode.hash(result, _2) + result = HashCode.hash(result, _3) + result = HashCode.hash(result, _4) + result = HashCode.hash(result, _5) + result + } + + override def equals(obj: scala.Any): Boolean = obj match { + case HttpRequest(_method, _uri, _headers, _entity, _protocol) => + method == _method && + uri == _uri && + headers == _headers && + entity == _entity && + protocol == _protocol + case _ => false + } + + override def toString = s"""HttpRequest(${_1},${_2},${_3},${_4},${_5})""" + + // name-based unapply accessors + def _1 = method + def _2 = uri + def _3 = headers + def _4 = entity + def _5 = protocol + } object HttpRequest { @@ -239,15 +282,28 @@ object HttpRequest { case _ ⇒ throw new IllegalArgumentException("""`uri` must have scheme "http", "https" or no scheme""") } } + + /* Manual Case Class things, to easen bin-compat */ + + def apply(method: HttpMethod = HttpMethods.GET, + uri: Uri = Uri./, + headers: immutable.Seq[HttpHeader] = Nil, + entity: RequestEntity = HttpEntity.Empty, + protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`) = new HttpRequest(method, uri, headers, entity, protocol) + + def unapply(any: HttpRequest) = new OptHttpRequest(any) } /** * The immutable HTTP response model. */ -final case class HttpResponse(status: StatusCode = StatusCodes.OK, - headers: immutable.Seq[HttpHeader] = Nil, - entity: ResponseEntity = HttpEntity.Empty, - protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`) extends jm.HttpResponse with HttpMessage { +final class HttpResponse( + val status: StatusCode, + val headers: immutable.Seq[HttpHeader], + val entity: ResponseEntity, + val protocol: HttpProtocol) + extends jm.HttpResponse with HttpMessage { + require(entity.isKnownEmpty || status.allowsEntity, "Responses with this status code must have an empty entity") require(protocol == HttpProtocols.`HTTP/1.1` || !entity.isInstanceOf[HttpEntity.Chunked], "HTTP/1.0 responses must not have a chunked entity") @@ -272,4 +328,58 @@ final case class HttpResponse(status: StatusCode = StatusCodes.OK, override def withEntity(entity: jm.RequestEntity): HttpResponse = withEntity(entity: jm.ResponseEntity) def mapEntity(f: ResponseEntity ⇒ ResponseEntity): HttpResponse = withEntity(f(entity)) -} \ No newline at end of file + + /* Manual Case Class things, to easen bin-compat */ + + def copy(status: StatusCode = status, + headers: immutable.Seq[HttpHeader] = headers, + entity: ResponseEntity = entity, + protocol: HttpProtocol = protocol) = new HttpResponse(status, headers, entity, protocol) + + + override def equals(obj: scala.Any): Boolean = obj match { + case HttpResponse(_status, _headers, _entity, _protocol) => + status == _status && + headers == _headers && + entity == _entity && + protocol == _protocol + case _ => false + } + + override def hashCode: Int = { + var result = HashCode.SEED + result = HashCode.hash(result, _1) + result = HashCode.hash(result, _2) + result = HashCode.hash(result, _3) + result = HashCode.hash(result, _4) + result + } + + override def toString = s"""HttpResponse(${_1},${_2},${_3},${_4})""" + + // name-based unapply accessors + def _1 = this.status + def _2 = this.headers + def _3 = this.entity + def _4 = this.protocol + +} + +object HttpResponse { + /* Manual Case Class things, to easen bin-compat */ + + def apply(status: StatusCode = StatusCodes.OK, + headers: immutable.Seq[HttpHeader] = Nil, + entity: ResponseEntity = HttpEntity.Empty, + protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`) = new HttpResponse(status, headers, entity, protocol) + + def unapply(any: HttpResponse): OptHttpResponse = new OptHttpResponse(any) +} + +final class OptHttpRequest(val get: HttpRequest) extends AnyVal { + def isEmpty: Boolean = get == null +} + +final class OptHttpResponse(val get: HttpResponse) extends AnyVal { + def isEmpty: Boolean = get == null +} diff --git a/akka-stream/src/main/scala/akka/stream/impl/io/InputStreamPublisher.scala b/akka-stream/src/main/scala/akka/stream/impl/io/InputStreamPublisher.scala index 4d601836ac..7527b4ace5 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/io/InputStreamPublisher.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/io/InputStreamPublisher.scala @@ -28,8 +28,8 @@ private[akka] object InputStreamPublisher { /** INTERNAL API */ private[akka] class InputStreamPublisher(is: InputStream, completionPromise: Promise[IOResult], chunkSize: Int) - extends akka.stream.actor.ActorPublisher[ByteString] - with ActorLogging { + extends akka.stream.actor.ActorPublisher[ByteString] + with ActorLogging { // TODO possibly de-duplicate with FilePublisher? diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index b4f76c4ba6..c3368df85b 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -532,6 +532,47 @@ object AkkaBuild extends Build { javacOptions in doc ++= Seq("-Xdoclint:none") ) + def akkaPreviousArtifacts(id: String): Def.Initialize[Set[sbt.ModuleID]] = Def.setting { + if (enableMiMa) { + val versions = { + val akka23Versions = Seq("2.3.11", "2.3.12", "2.3.13", "2.3.14") + val akka24Versions = Seq("2.4.0", "2.4.1", "2.4.2") + val akka24NewArtifacts = Seq( + "akka-cluster-sharding", + "akka-cluster-tools", + "akka-cluster-metrics", + "akka-persistence", + "akka-distributed-data-experimental", + "akka-persistence-query-experimental" + ) + scalaBinaryVersion.value match { + case "2.11" if !akka24NewArtifacts.contains(id) => akka23Versions ++ akka24Versions + case _ => akka24Versions // Only Akka 2.4.x for scala > than 2.11 + } + } + + // check against all binary compatible artifacts + versions.map(organization.value %% id % _).toSet + } + else Set.empty + } + + def akkaStreamAndHttpPreviousArtifacts(id: String): Def.Initialize[Set[sbt.ModuleID]] = Def.setting { + if (enableMiMa) { + val versions = { + val akka24Versions = Seq("2.4.2") + val akka24NewArtifacts = Seq( + "akka-http-core" + ) + + akka24Versions + } + + // check against all binary compatible artifacts + versions.map(organization.value %% id % _).toSet + } else Set.empty + } + def loadSystemProperties(fileName: String): Unit = { import scala.collection.JavaConverters._ val file = new File(fileName) diff --git a/project/MiMa.scala b/project/MiMa.scala index adba135c98..e1361c6485 100644 --- a/project/MiMa.scala +++ b/project/MiMa.scala @@ -675,6 +675,33 @@ object MiMa extends AutoPlugin { ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.RequestEntity.withoutSizeLimit"), ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.UniversalEntity.withoutSizeLimit"), ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.ResponseEntity.withoutSizeLimit"), + // #19956 Remove exposed case classes in HTTP model + ProblemFilters.exclude[MissingTypesProblem]("akka.http.scaladsl.model.HttpRequest$"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.http.scaladsl.model.HttpRequest.unapply"), // returned Option[HttpRequest], now returns HttpRequest – no Option allocations! + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.$default$1"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.$default$2"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.$default$3"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.$default$4"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.$default$5"), + ProblemFilters.exclude[MissingTypesProblem]("akka.http.scaladsl.model.HttpResponse"), // was a case class (Serializable, Product, Equals) + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.productElement"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.productArity"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.canEqual"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.productIterator"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.productPrefix"), + + ProblemFilters.exclude[MissingTypesProblem]("akka.http.scaladsl.model.HttpRequest"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.productElement"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.productArity"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.canEqual"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.productIterator"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpRequest.productPrefix"), + ProblemFilters.exclude[MissingTypesProblem]("akka.http.scaladsl.model.HttpResponse$"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.http.scaladsl.model.HttpResponse.unapply"), // returned Option[HttpRequest], now returns HttpRequest – no Option allocations! + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.$default$1"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.$default$2"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.$default$3"), + ProblemFilters.exclude[MissingMethodProblem]("akka.http.scaladsl.model.HttpResponse.$default$4"), // #20014 should have been final always ProblemFilters.exclude[FinalClassProblem]("akka.http.scaladsl.model.EntityStreamSizeException"),