Releases: finagle/finch
Finch 0.17
This release features Finagle 18.2 and the brand new support to server-driven content negotiation.
Content Negotiation
An idea of supporting a content negotiation initially came up about two years ago, and it's finally become a reality (thanks to @imliar's inspiring contribution!). As of 0.17, it's possible to specify the list of supported content-types per endpoint and have the appropriate encoders be resolved at compile time. An actual encoder is dispatched later, at runtime, when client's Accept
header value is known.
Content negotiation is disabled by default (for now). Use Bootstrap.configure
to enable it:
import io.finch._
val s = Bootstrap.configure(negotiateContentType = true)
.serve[Application.Json :+: Text.Plain :+: CNil](Endpoint.lift("Hello, World!"))
.toService
In the example above, the "Hello, World!" endpoint may be served within both JSON and plain text payloads, depending on the Accept
header value sent by a client. If nothing is provided, it defaults to the first content-type in the list (actually, a coproduct). Note: this program won't compile until both encoders are resolved.
Explicit Syntax (verb matching)
We tried to make the transition from implicit to explicit syntax as smooth as possible but it caused some non-trivial shadowing issues in the 0.16 release. Based on that, we made an executive decision to break an API (a little) and stop supplying a default syntax API in the io.finch._
import. Enable verb-matching syntax explicitly by importing io.finch.syntax._
.
Finch 0.16.1
This is patch-release for Finch 0.16 that fixes a couple of io.finch.syntax
problems:
- #896: deprecated syntax import (
io.finch._)
) conflits with exmplicit import (io.finch.syntax._
) - #895:
import io.finch.syntax._
shadowsscala
namespace
We decided to un-deprecate the implicit syntax coming from io.finch._
as it's the only way to avoid breaking API change. So the previous style should work just fine:
import io.finch._
val foo = get("foo") { Ok("bar") }
To enable the Scala Futures support, simply add an import io.finch.syntax.scalaFutures._
.
Finch 0.16
The first release against Cats 1.0 (and the rest of the ecosystem) that also includes six milestone releases:
The following dependencies were bumped:
- Cats to 1.0.1
- Circe to 0.9
- Finagle to 17.12
Circe Error Accumulation
In addition to the six milestone releases, the final release includes a new shiny feature that allows accumulating errors returned from a Circe decoder (i.e., report multiple parse errors at once). See #884 (thanks @imliar) for more details. To enable error accumulation, replace io.finch.circe._
import with:
import io.finch.circe.accumulating._
Finch 0.16-M6
(Hopefully) the last milestone release for 0.16 (which will be released in against Cats 1.0 once it's available):
- Cats 1.0-RC2 and Circe 0.9-M3
- Finagle/Util 17.12
Multiple file-uploads & attributes
This milestone release accompanied by a new set of endpoints to read multiple file-uploads (thanks @jguitana, see #811) and multipart attributes (see #872).
import io.finch._
scala> val foo = multipartAttributesNel("foo")
foo: io.finch.Endpoint[cats.data.NonEmptyList[String]] = attributes(foo)
scala> val bar = multipartFileUploadsNel("bar")
bar: io.finch.Endpoint[cats.data.NonEmptyList[com.twitter.finagle.http.exp.Multipart.FileUpload]] = bar
Deprecating .as[Foo]
syntax
We're deprecating an old style type-converters available via the .as[A]
syntax extension (thanks @imliar, see #875). Instead, we're offering a new API that accepts a target type at the moment endpoint is created. This is supported by param(s)
, header
, and multipartAttribute(s)
endpoints.
scala> val p = param("p").as[Int]
<console>:38: warning: method as in class StringEndpointOps is deprecated (since 0.16)
val p = param("p").as[Int]
^
p: io.finch.Endpoint[Int] = param(p)
scala> val q = param[Int]("q")
q: io.finch.Endpoint[Int] = param(q)
The new API isn't just easier to write/read but also more efficient as it allows to skip an interim endpoint creation (needed to accommodate the as
operation).
Note:
param("foo")
(no type-parameter) is still supported and defaults toparam[String]("foo")
.Endpoint[L <: HList]
->Endpoint[Foo]
conversion is still supported via the standart means ofas[Foo]
operation.
Deprecating implicit Sinatra-like syntax
After 0.16
, Finch's Sinatra-like syntax should be explicitly imported (see #876).
scala> import io.finch._
import io.finch._
scala> val p = get(/) { Ok("foo") }
val p = get(/) { Ok("foo") }
<console>:14: warning: method get in trait DeprecatedEndpointMappers is deprecated (since 0.16): Enable syntax explicitly: import io.finch.syntax._
val p = get(/) { Ok("foo") }
^
p: io.finch.Endpoint[String] = GET /
scala> import io.finch.syntax._
import io.finch.syntax._
scala> val p = get(/) { Ok("foo") }
val p = get(/) { Ok("foo") }
p: io.finch.Endpoint[String] = GET /
Finch 0.16-M5
Another milestone release for Finagle 17.11.
Improved Multipart Support
Multipart attributes are now decoupled from param(s)
endpoints as they were instigating a multipart decoding on each optional param (see #860 for discussion and #865 for a fix).
scala> val foo = multipartAttribute("foo")
foo: io.finch.Endpoint[String] = foo
scala> val bar = multipartFileUpload("bar")
bar: io.finch.Endpoint[com.twitter.finagle.http.exp.Multipart.FileUpload] = bar
Circe's Printing Size Predictor
Circe's size predictor allows saving a couple of copy-and-grow iterations while printing into a binary output. It uses a very primitive form of a feedback controller to grow/shrink a potentially expected size based on the history of allocations (see circe/circe#739 for more details).
We've seen quite a great improvements in our ToServiceBenchamrk
(around ~2x of throughput improvement) yet we were unable to reproduce the numbers with the wrk
(i.e., end-to-end test). Based on that, this option should be considered experimental (use with caution and run your own tests).
Size prediction could be enabled with io.finch.circe.predictSize._
import (see #868).
Finch 0.16-M4
A new milestone release that brings updated dependencies
Better Intellij Support
We've continued making progress towards better Intellij integration. It's been a persistent problem when IDEA marks compilable endpoints "red" (as it was reported in #631 about a year ago). The main issue is a magnet-pattern machinery that Finch relies upon for its syntax. We've filed a Scala plugin issue with Intellij to make sure they look into that as well yet it wasn't getting lots of JetBrains attention by far. With 31 votes (this is beyond awesome!) it's among the most highest-voted tickets Scala plugin has ever had.
We also tried to make some progress on our own and simplify the constructions we've been using. @imliar has done a decent job introducing ToTwitterFuture
(see #834) and improving the overall highlighting experience later (see #858).
While we're not completely there yet, but we're confident in its current state Finch users should have much better experience with Intellij.
Finch 0.16-M3
Another (hopefully the last one) milestone release before 0.16. This time with Scala Futures syntax support.
Scala Syntax Support
In #834, @imliar suggested a new type-class, ToTwitterFuture
to abstract over a concrete IO primitive powering endpoint's asynchrony. Right now there are only Scala Futures instances available for ToTwitterFuture
, but one can imagine this approach to be scaled to any Futures implementations available for Scala.
Importing io.finch.syntax.scala._
allows to scala.concurrent.Future
from within endpoints (assuming ExecutionContext
is available in the scope).
import io.finch._, io.finch.syntax.scala._
import scala.concurrent.{Future, ExecutionContext.Implicits.global}
scala> val e = get("foo") { Future.successful(Ok("bar")) }
e: io.finch.Endpoint[String] = GET /foo
scala> e(Input.get("/foo")).awaitValueUnsafe()
res0: Option[String] = Some(bar)
Deprecations
Old-style path matchers such as int
, string
, boolean
, etc are being deprecated in favor of more explicit path[Int]
, path[String]
, path[Boolean]
and so on. We believe new API will make path matchers more discoverable and easier to reason about. See #844 for more context.
Dependencies
- Finagle has been bumped to 7.1 (see #842)
Finch 0.16-M2
This milestone release brings new performance improvements (reducing the gap between Finagle and Finch to only 5%) as well as new finch-iteratee
module enabling generic request/response streaming.
Iteratee Streaming
Integration with iteratee library was one of the most wanted features in Finch. Travis Brown (@travisbrown) started experimenting with it almost a year ago (see #557) yet that work was only finished now (see #812), by a tireless effort of Sergey Kolbasov (@imliar).
Essentially it's now possible to
- receive chunked payloads in a form of
io.iteratee.Enumerator[Future, A]
(when it's known how toio.finch.iteratee.Enumerate[CT, A]
) - send chunked payloads in a form of
io.iteratee.Enumerator[Future, B]
(when it's known how toio.finch.Encode.Aux[CT, B]
)
For example, importing both
import io.finch.circe._, io.finch.iteratee._
enables new-line-delimited JSON streaming in Finch.
import io.finch._
import io.finch.circe._
import io.finch.iteratee._
import io.circe.generic.auto._
import io.iteratee._
import com.twitter.util.Future
case class Foo(bar: Int)
// Streaming request payload.
val sum: Endpoint[Int] =
post("foos" :: enumeratorJsonBody[Foo]) { (foos: Enumerator[Future, Foo]) =>
val ints: Enumerator[Future, Int] = foos.through(map[Foo, Int](_.bar))
val sum: Future[Int] = ints.into(fold[Int, Int](0)(_ + _))
sum.map(Ok) // a future will be completed when whole stream is folded
}
// Streaming response payload.
val foos: Endpoint[Enumerator[Future, Foo]] = get("foos") {
Ok(enumList(List(Foo(1), Foo(2)))
}
See more details in the docs.
Performance Improvements
I learned this simple technics allowing to reduce allocations around closures from Flavio Brasil (@fwbrasil). Applying them to Finch yield quite significant performance gain (see #807) reducing the gap between Finch and Finagle to just 5% (was 10-12% before).
Here are some numbers for running time (less is better):
NEW:
MapBenchmark.map avgt 10 633.837 ± 14.613 ns/op
MapBenchmark.mapAsync avgt 10 624.249 ± 22.426 ns/op
MapBenchmark.mapOutput avgt 10 734.677 ± 30.215 ns/op
MapBenchmark.mapOutputAsync avgt 10 737.170 ± 29.808 ns/op
ProductBenchmark.bothMatched avgt 10 1175.716 ± 44.236 ns/op
ProductBenchmark.leftMatched avgt 10 26.510 ± 2.335 ns/op
ProductBenchmark.rightMatched avgt 10 5.081 ± 0.112 ns/op
OLD:
MapBenchmark.map avgt 10 624.174 ± 28.621 ns/op
MapBenchmark.mapAsync avgt 10 647.369 ± 30.775 ns/op
MapBenchmark.mapOutput avgt 10 1221.053 ± 39.319 ns/op
MapBenchmark.mapOutputAsync avgt 10 1202.541 ± 44.432 ns/op
ProductBenchmark.bothMatched avgt 10 1224.278 ± 49.114 ns/op
ProductBenchmark.leftMatched avgt 10 29.856 ± 0.709 ns/op
ProductBenchmark.rightMatched avgt 10 5.209 ± 0.112 ns/op
In a nutshell:
map*
operations are now up to 40% faster (400 bytes fewer allocations on each call)::
is now up to 5% faster (120 bytes fewer allocations on each call)
Dependencies
- Finagle 7.0
- Circe 0.9.0-M1
- Cats 1.0.0-MF
Finch 0.16-M1
Meet Bootstrap
The intention of this early 0.16 release (hence M1) is to allow people to experiment with the new Bootstrap
abstraction that supposed to allow serving multiple content-types out of a single Service
(i.e., coproduct endpoint). This was one of the most-wanted features since the type-level content type was shipped, over a year ago.
See the following tickets for more context/history on the subject:
- Content-Type as a Type #541
- Supporting multiple Content-Types in a single endpoint/service #575
- ServiceBulder (abandoned) #583
- Bootstrap (merged) #794
In a nutshell, the new Bootstrap
abstraction is pretty much an evolution of ServiceBuilder
(see #583) that also has a notion of a configuration params for derived services (i.e., Service[Request, Response]
). At this point, we only support two options: includeServerHeader
(enabled by default) and includeDateHeader
(enabled by default), but one can easily imagine this machinery to be scaled to support a wide spectrum of use cases:
The quick start example looks pretty straightforward:
import io.finch._
import io.finch.circe._
import io.finch.generic._
import com.twitter.finagle.Http
scala> val json = get("json") { Ok(Map("foo" -> "bar")) }
json: io.finch.Endpoint[scala.collection.immutable.Map[String,String]] = GET /json
scala> val text = get("text") { Ok("Hello, World!") }
text: io.finch.Endpoint[String] = GET /text
scala> val s = Bootstrap.configure(includeServerHeader = false)
.serve[Application.Json](json)
.serve[Text.Plain](text)
.toService
s: com.twitter.finagle.Service[com.twitter.finagle.http.Request,com.twitter.finagle.http.Response] = <function1>
scala> Http.server.serve(":8081", s)
Jun 15, 2017 5:35:01 PM com.twitter.finagle.Init$$anonfun$5 apply$mcV$sp
INFO: Finagle version 6.45.0 (rev=fadc80cdd804f2885ebc213964542d5568a4f485) built at 20170609-103047
res1: com.twitter.finagle.ListeningServer = com.twitter.finagle.server.ListeningStackServer$$anon$1@1da24919
And then using HTTPie:
$ http :8081/text
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Fri, 16 Jun 2017 00:35:57 GMT
Hello, World!
$ http :8081/json
HTTP/1.1 200 OK
Content-Type: application/json
Date: Fri, 16 Jun 2017 00:36:01 GMT
{
"foo": "bar"
}
If you need more examples, here is how Finch TechEmpower benchmark is evolved after this release: TechEmpower/FrameworkBenchmarks#2869
Other changes
Finch 0.15.1
A patch release that contains two fixes/additions:
- #788: Upgrading Catbird dependency and Scala versions (thanks @travisbrown )
- #796: Fixing optional bodies so they return
None
on empty payloads (thanks @JoeEnnever)