Skip to content

Releases: finagle/finch

Finch 0.15

09 Jun 23:01
Compare
Choose a tag to compare

Netty 4 is a default now

Finagle 6.45 brings HTTP implementation based on Netty 4. It's now a default transport for both servers and clients created with Finagle. At this point, it's known that finagle-http running Netty 4 might be performing slightly worse than Netty 3 given the overhead the outdated Netty 3-based HTTP model involves. The Finagle team is aware of that and is working hard to remove the Netty 3 dependency from finagle-http hence make it faster with Netty 4.

If you're cautious about the performance degradations (roughly 15%-20% slower on our "Hello, World!" benchmark) during the upgrade, stick to Netty 3 for now (as shown below):

import com.twitter.finagle.Http

val server = Http.server.configured(Http.Netty3Impl)

Default exception encoders

We decided to stop supplying the default instances of exception JSON encoders (i.e., Encode.Json[Exception]) given the amount of confusion and ambiguity they bring on the table (examples: #765, #737). The default behavior now (unless an instance of Encode is provided) is to encode exceptions to just empty payloads.

New syntax package

We're starting to decouple Endpoints from their syntax (i.e., Mapper). The fact they were coupled together caused lots of surprising compile errors in the past (especially in the Scala 2.10 times) and we're quite happy to start drawing a line between a concept and its syntax. As of this release, there are no breaking changes such that the Endpoint.apply(Mapper) is still supported (although not directly) but we're planning to start hiding things behind the io.finch.syntax namespace in the next release. See #779 for more details.

New API for path matching

We're promoting a new and more explicit API for path matching. Now there is a path method that acts as following (see #771 for more details):

  • path("foo") - matches a path "foo" (was "foo": Endpoint[HNil] before)
  • path[String] - extracts a string path (was string before)

Note that the previous API (i.e., int, string, etc) is still supported and we're pending its depreciation given the successful adoption of the new API.

While the new way is definitely more verbose (see example bellow) we think it is much more explicit and clear for the reader. At some point (when we merged request readers with endpoints), symbols as int, string, etc become ambiguous (i.e., does int means "path as int" or "param as int") and are not something we'd like to see as part of 1.0 API (stable API).

val a = get("add" :: path[Int] :: path[Int]) { (a: Int, b: Int) => Ok(a + b) } // new
val b = get("bar" :: int :: int) { (a: Int, b: Int) => Ok(a + b) } // old

Other changes

  • io.finch.Input is now fully immutable such that all calls to its with methods produces a new Input (see #766)
  • The performance degradation with binary printing is now fixed as of Circe 0.8 (see #772)

finch-core/finch-circe 0.14.1

25 Apr 03:44
Compare
Choose a tag to compare

This release only publishes finch-core & finch-circe artifacts and rolls back string-less encoding for Circe. While it was known that byte-targeted printing might be a little slower for small/tiny input (see circe/circe#542), we didn't realize it might hit Finch really hard in the TechEmpower benchmark (-30%).

We're going to put string-less encoding back when we know more about the issue.

Finch 0.14

31 Mar 17:35
Compare
Choose a tag to compare

HTTP/2 Support

This release brings experimental HTTP/2 support via Finagle 6.43.

As usual, there are two ways enabling it in Finch's applications: via a toggle override or programmatically. Supply the following CLI flag to globally switch each HTTP client/server running in the same process:

-com.twitter.finagle.toggle.flag.overrides=com.twitter.finagle.http.UseHttp2=1.0

Or control the HTTP version on a per-client/per-server basis:

import io.finch._
import com.twitter.finagle.Http
import com.twitter.util.Await

Await.ready(
  Http.server
    .configuredParams(Http.Http2)
    .serve(":8081", Endpoint.lift("foo").toServiceAs[Text.Plain])
)

New Site

Finch's user guide, best practices, and the cookbook are now published within the microsite with all the source code snippets type-checked by tut (thanks @erikwj, see #751).

Other changes

  • HTTP Date header returned from a Finch service is now conforms the spec format (thanks @rpless, see #752)
  • New Finch module finch-generic now provides some basic machinery allowing for generic derivation of endpoints (thanks @ilya-murzinov, see #716).

Finch 0.13.1

18 Feb 18:53
Compare
Choose a tag to compare

This patch-release contains a fix to #747 (decoding errors with Netty 4). Huge thanks to @rpless for reporting the bug and verifying the fix.

Note: no need to update from 0.13 if you're not using Netty 4.

Finch 0.13

11 Feb 21:53
Compare
Choose a tag to compare

This release is intended to accommodate Finagle 6.42 with its brand-new Netty 4 support. As of Finch 0.13, you may now (and should!) switch the underlying transport implementation over to Netty 4.

Netty 4 support

Historically, Finch was using some Netty 3-specific optimizations (i.e., hacks) to perform zero-copy retrieving of the HTTP payloads. In this release, we made everything Netty-agnostic and made sure it's still as efficient as it was before. This means you may now consider switching the underlying transport over to Netty 4 not worrying about potential performance penalties.

Switching to Netty 4 right now means staying aligned with future resiliency and performance improvements in finagle-http. At this point, there is no known difference in the throughput between finagle-http servers running Netty 3 and Netty 4. However, several major improvements for Netty 4 HTTP transport are planned to be shipped in the future Finagle releases. To mention a few: memory pooling (fewer allocations) and HTTP/2 support.

To jump into the Netty 4 land, supply the following CLI flag to your Finch application:

-Dcom.twitter.finagle.toggle.flag.overrides=com.twitter.http.UseNetty4=1.0

Or specify Netty 4 implementation programmatically on your Http.Server instance:

import io.finch._
import com.twitter.finagle.Http
import com.twitter.util.Await

Await.ready(
  Http.server
    .configured(Http.Netty4Impl)
    .serve(":8081", Endpoint.lift("foo").toServiceAs[Text.Plain])
)

Other changes

Finch 0.12

18 Jan 03:10
Compare
Choose a tag to compare

Scala 2.12

This release finally brings Scala 2.12 support (see #668, thanks @ilya-murzinov, @travisbrown, @clhodapp). As for 0.12, Finch is cross-published for both 2.11 and 2.12.

String-less Encoding with Circe/Jackson

Since 0.11, Finch parses JSON directly from bytes (not strings), which improved its throughput quite dramatically (see #671). This time we're moving towards "string-less encoding" (see #676) with Circe and Jackson by printing directly into a byte buffer and returning that as an HTTP payload (see #717 and #714, thanks @imliar, @travisbrown).

The benchmark numbers look inspiring and demonstrate quite well how the initial idea of hiding the serialization/deserialization logic behind functional abstractions is starting paying back. Being able to control this part of request lifecycle allows for optimizations specific to a concrete JSON library.

Overall, the numbers confirm that printing directly to bytes cuts allocations in half (and improves the throughput).

circe-core (string)

[info] Benchmark                                               Mode  Cnt        Score        Error   Units
[info] ToServiceBenchmark.foos                                thrpt   20      463.981 ±     24.913   ops/s
[info] ToServiceBenchmark.foos:·gc.alloc.rate.norm            thrpt   20  5002135.084 ±   3252.704    B/op
[info] ToServiceBenchmark.ints                                thrpt   20     2118.919 ±     49.846   ops/s
[info] ToServiceBenchmark.ints:·gc.alloc.rate.norm            thrpt   20   873272.466 ±    106.930    B/op

circe-core (bytes)

[info] Benchmark                                               Mode  Cnt        Score        Error   Units
[info] ToServiceBenchmark.foos                                thrpt   20      523.730 ±     21.871   ops/s
[info] ToServiceBenchmark.foos:·gc.alloc.rate.norm            thrpt   20  2602833.269 ±     76.806    B/op
[info] ToServiceBenchmark.ints                                thrpt   20     2234.631 ±    111.156   ops/s
[info] ToServiceBenchmark.ints:·gc.alloc.rate.norm            thrpt   20   545837.968 ±  43806.910    B/op

EndpointResult

Previously, Endpoint result was modeled as Option[Tuple2[Input, Rerunnable[Output[_]]]] indicating that result could be either skipped (i.e., None) or matched and both remainder and the output is returned. This release introduces a new type EndpointResult (see #707) that encodes this very behavior as an ADT with two cases Matched and Skipped.

A standalone abstraction not only simplifies the reasoning about endpoints but also improves their performance. Technically, EndpointResult is a flattened version of Option[Tuple2[_, _]] so instead of two nested objects we only need one to carry the result. This simple idea impacts nearly all endpoint operations slightly reducing allocations and improving throughput (up to 5% better on some benchmarks).

Here are the numbers from map* variants (before and after):

[info] MapBenchmark.mapAsync                               avgt    6   429.113 ±   43.297   ns/op
[info] MapBenchmark.mapAsync:·gc.alloc.rate.norm           avgt    6   776.000 ±    0.001    B/op
[info] MapBenchmark.mapAsync                               avgt    6   407.126 ±   12.807   ns/op
[info] MapBenchmark.mapAsync:·gc.alloc.rate.norm           avgt    6   720.000 ±    0.001    B/op

[info] MapBenchmark.mapOutputAsync                         avgt    6   821.786 ±   52.045   ns/op
[info] MapBenchmark.mapOutputAsync:·gc.alloc.rate.norm     avgt    6  1376.001 ±    0.001    B/op
[info] MapBenchmark.mapOutputAsync                         avgt    6   777.654 ±   26.444   ns/op
[info] MapBenchmark.mapOutputAsync:·gc.alloc.rate.norm     avgt    6  1320.001 ±    0.001    B/op

Better Testing APIs

Methods for querying a rerunnable Output returned from an Endpoint have been renamed to be explicitly alerting about their blocking nature. Now they all prefixed with await and take an optional Duration indicating the upper bound of await time (previously 10 seconds).

Deprecation schema:

  • value -> awaitValueUnsafe()
  • tryValue -> awaitValue()
  • output -> awaitOutputUnsafe()
  • tryOutput -> awaitOutput()

Basic HTTP Auth

Finch's implementation of Basic HTTP Auth has been moved to its own project finagle-http-auth and promoted to Finagle filter so it could be used with bare metal finagle-http.

To enable the Basic HTTP Auth on Finch's endpoint, apply the BasicAuth.Server filter to the service yield from toService call.

scala> import com.twitter.finagle.http.BasicAuth, io.finch._

scala> val ba = BasicAuth.serverFromCredentials("admin", "12345")
ba: com.twitter.finagle.http.BasicAuth.Server = <function2>

scala> val s = ba.andThen(Endpoint.const("foo").toServiceAs[Text.Plain])
s: com.twitter.finagle.Service[Request, Response]

Other Changes

  • New method Endpoint.liftToTry that works in a similar fashion to Future.liftToTry (see #710)
  • New method Endpoint.productWith that also accepts a function (A, B) => C (see #692, thanks @imliar)
  • Jackson's ObjectMapper is now embedded in io.finch.jackson. No need for an implicit instance anymore (see #714)

Dependencies

  • Finagle 6.41
  • Circe 0.7
  • Cats 0.9
  • Shapeless 2.3.2

Finch 0.11.1

15 Dec 23:42
Compare
Choose a tag to compare

A patch release with couple of bug-fixes (and updated Jackson dependency)

Finch 0.11

13 Dec 19:06
Compare
Choose a tag to compare

NOTE: If you're upgrading from 0.10, see changes in the milestone releases as well:

Goodbye Scala 2.10 and Java 7

We've finally decided to drop 2.10 and Java 7 support and align with the most recent version of Finagle (6.40 at this point). See #686. For the reference, Finagle and Co dropped 2.10 support in 6.35 (five releases ago).

Server Sent Events

Finch now provides very basic support to SSE. See #655 and the new cookbook example (thanks @rpless).

Long story short, serving AsyncStream[io.finch.sse.ServerSentEvent[A]], for which, cats.Show[A] is defined will stream generated events back to the client until the connection is closed or the stream is empty.

New Decoding Machinery

In 0.11-M1 we've made a decent progress on making encoding in Finch less magical by introducing a type-level content type. This removed a lot of ambiguity when more than one implicit encoder present in the scope.

This time, we did a similar trick for Decode type class. Now it embeds a type-level string indicating a content-type this decoder can decode. This highlighted an obvious difference between decoding query-string param as Int and HTTP body as JSON. These are clearly two different use case that should be handled separately. Thus there is a new type class for decoding HTTP entities (not bodies) in Finch: DecodeEntity that (1) doesn't embed a content-type and (2) decodes from String. Decode, on the other hand, know about content-type and (now) decodes from Buf (as it should). See #663 for more details.

This work not only made the decoding story in Finch more explicit (clear separation of concerns, decoders for HTTP bodies are now resolved w.r.t. their content-types) but also allowed a performance optimization for body* endpoints. Given that we now can decode directly from Buf, we can eliminate one extra copy of going from Buf to String (to satisfy the DecodeEntity input type). At this point, we managed to improve the throughput on out end-to-end test by 12.5%. See #671 (thanks @rpless) for more details.

All these benefits came with the cost of breaking API. See API changes for more details.

Decoding with Content-Type

Now that we have type-level content-type in place, we can enforce the implicit resolution to pick the right decoder for an expected request. We introduce a new API for body* endpoints that also accept the desired content-type. See #695 for more details.

NOTE: body.as[User] is still supported and defaults to body[User, Application.Json].

// before (deprecated in this version)
body.as[User]

// after
body[User, Application.Json]
// or using an alias method
jsonBody[User]

Previously, it was also possible to run as[User] to decode a User from JSON sent as a query string param or header. This indeed really powerful and allows some questionable design patterns, which are not necessary useful. Instead of being super generic here, we're trying to reduce the number of ways things could be built. This not only makes them easy to reason about but also quite efficient (b/c you now, specialization).

That said, we're promoting a new way of decoding HTTP payloads. Instead of using .as[A], we make it less powerful and more explicit. By limiting the responsibility of the Decode type-class, we tight it directly with HTTP payloads. This means constructing body* endpoints could be done in a single step, instead of producing 3-nested structures hence reduce allocations.

Quick experiments showed that we could save 15% of running time and 20% of allocations by just beeing explicit (json2 is the new body[A, Application.Json], json is body.as[A]).

[info] BodyBenchmark.json                       avgt    6  4824.580 ± 1205.444   ns/op
[info] BodyBenchmark.json:·gc.alloc.rate.norm   avgt    6  5896.004 ±  147.449    B/op
[info] BodyBenchmark.json2                      avgt    6  4179.209 ±  673.098   ns/op
[info] BodyBenchmark.json2:·gc.alloc.rate.norm  avgt    6  4936.004 ±   73.723    B/op

[info] BodyBenchmark.jsonOption                       avgt    6  4335.755 ±  150.928   ns/op
[info] BodyBenchmark.jsonOption:·gc.alloc.rate.norm   avgt    6  5712.004 ±    0.001    B/op
[info] BodyBenchmark.jsonOption2                      avgt    6  4050.681 ±  685.263   ns/op
[info] BodyBenchmark.jsonOption2:·gc.alloc.rate.norm  avgt    6  4940.004 ±   36.862    B/op   

Fixing Mistakes in Errors

Some of the Finch's core components, including its error types, were designed a couple of years ago. Mistakes were made. We acknowledge it and fixing them now. See #694 for the full discussion.

Here is the summary of what's changed:

  • A misleading type for error accumulation (RequestErrros) now represents a flat non-empty list of Finch's own errors (previously, a recursive Seq of generic Throwables). Technically, the new type tells exactly what happens at runtime (which is exactly why need types) - we always flatten errors while collecting, not nest them.
  • Now product endpoint only accumulates Finch's own errors and fails-fast with the first non-Finch error observed. We think this is a sane default behavior given that's not safe to keep evaluating endpoints while one of them failed with an unknown reasons that could have side-affected an entire application.

Lift Your Stuff

Endpoint.liftX is a collection of factory methods allowing to build Finch Endpoints out of anything. Presumably, these could be useful for wrapping functions returning arbitrary value within an [Endpoint] context.

// The following endpoint will recompute a random integer on each request.
val nextInt: Endpoint[Int] = Endpoint.lift(scala.util.random.nextInt)

Behaviour Changes

  • Finch now defines a very basic instance of Encode.Aux[Exception, ?] that is polymorphic to the content type. This means, if no Encode[Exception] is provided for a given content-type, Finch app will still compile using this default instance (see #683).

API Changes

  • body and bodyOption endpoints now return Buf instead of String. Thus body.as[User] should still work as expected, but there is a new stringBody instance that might be used in place of body where a UTF-8 string of an HTTP body is expected.
  • Encode instance for Either is removed (this shouldn't be defined in Finch). See #689.

Bug Fixes

Finch 0.11-M4

17 Oct 03:57
Compare
Choose a tag to compare
  • Endpoints testing now easier with polymorphic withBody (see #646)
  • Simplifying the behavior around empty entries (see #642)
  • Better error messages in finch-circe (see #648)

Polymorphic Input.withBody

It's now possible to utilize content-types (type-level strings) in Input.withBody such that a proper encoder (i.e., io.finch.Encode) is picked for the arbitrary value.

Before:

Input.post("/").withBody(Buf.Utf8("text"), Some("text/plain;charset=utf8"))
Input.post("/").withJson(Map("a" -> "b"), Some(Charsets.Utf8))

Now:

// Now:
Input.post("/").withBody[Text.Plain]("text", Some(Charsets.Utf8))
Input.post("/").withBody[Application.Json](Map("a" -> "b"), Some(Charsets.Utf8))

// We get the following for free:
import cats.instances.int._
Input.post("/").withBody[Text.Plain](42)

Empty strings are now treated as Some(""), not None

For the sake of principle of least astonishment, we decided to simplify Finch's core endpoints such as header, param, and body so they resolve into Some("") if the given entity is represented as an empty string (None before). This makes these endpoints both more flexible and less powerful (and actually more lightweight).

Finch 0.11-M3

31 Aug 17:54
Compare
Choose a tag to compare
  • Endpoint[Response] is now a first class citizen (see #624)
  • The behavior for params endpoint is changed a bit to make it easier to reason about (see #625)
  • There is new API for testing endpoints (see #619)
  • Cats updated to 0.7.0

Downgrading Endpoints

This release makes it easier to work with raw Finagle Responses in the context of Finch Endpoints such that they can be used instead of Outputs.

This now works (no need to wrap with Output):

val foo: Endpoint[Response] = get(/) { Response(Status.Ok) }

Finagle Responses play an essential role in Finch's applications when it's much easier to take a shortcut and deal with HTTP types directly. This is why their support should be vital. For example, until 0.11 is here, dealing with raw Responses is the only way to serve multiple content types within a single service.

Endpoints Testing

This release makes it easier to test Endpoints by proving a lightweight API for Input and exposing some useful methods on Output.

Here is an example of how Input.post and Output.value might be used to write simple tests similar to how functions are tested:

scala> val sum: Endpoint[Int] = post(int :: int) { (a: Int, b: Int) => Ok(a + b) }
sum: io.finch.Endpoint[Int] = POST /:int/:int

scala> sum(Input.post("/10/20")).value == Some(30)
res1: Boolean = true

scala> sum(Input.get("/foo/bar")).value == None
res2: Boolean = true