-
Notifications
You must be signed in to change notification settings - Fork 221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Finch in Action #204
Comments
Would be nice to hear others opinion on this direction. /cc @BenWhitehead, @jenshalm, @rpless, @rodrigopr, @benjumanji. |
This seems like a nice idea. I was just writing some code (not yet finch'd) that basically involved a bunch of filters to turn |
Although, it's not clear yet how to fetch the details from a custom request type using this approach. case class MyReq(http: HttpRequest, currentUserId: Long) |
I've came up with an interesting idea. The answer is in an a cornerstone abstraction that should glue all the basic blocks together. This approach doesn't involve changes of the current abstractions but adds new one that wires everything together. I'm thinking about adding trait Microservice1[-A, +B] {
def apply(a: A): Future[B]
}
...
trait Microservice5[-A, -B, -C, -D, -E, +F] {
def apply(a: A, b: B, c: C, d: D, e: E): Future[F]
}
type Microservice[-Req, +Rep] = Microservice1[Req, Rep] Microservice is a lightweight and domain-specific version of a Finagle Microservices are composable with both request readers and response builders. Request readers change its input type to val r: RequestReader[A ~ B ~ C] = ???
val m1: Microservice[A, B, C, D] = ???
val m2: Microservice[HttpRequest, D] = m1 compose r
val m3: Microservice[HttpRequest, HttpResponse] = m2 andThen Ok Thus, the data-flow is something like this ( It's obvious, that microservcies are composable with both simple functions and other microservices in a familiar way: trait Microservice1[-A, +B] {
def apply(a: A): Future[B] = ???
def compose(f: A => C): Microservice[C, B] = ???
def andThen(f: B => C): Microservice[A, C] = ???
def compose[C](before: Microservice[C, A]): Microservice[C, B] = ???
def andThen[C](after: Microservice[B, C]): Microservice[A, C] = ???
} Finally, the problem with custom request type might be easy resolved with a version of a trait Microservice1[-A, +B] { self
def apply(a: A): Future[B]
def compose[Req](f: Req => RequestReader[A])(implicit ev: Req => HttpRequest) =
new Microservice1[Req, B] {
def apply(req: Req) = f(req)(req) flatMap self
}
}
case class MyReq(http: HttpRequest, currentUserId: Int)
val getUserName: Microservice[Int, String] = ???
val m1: Microservice[MyReq, Int] = m.compose { req: MyReq => RequestReader.value(req.currentUserId) }
val m2: Microservice[MyReq, HttpResponse] = m andThen Ok We've seen before how microservices are glued request readers and response builders together. The last thing we have to think about is routers. We might redefine an endpoint in term of microservices to allow them be implicitly converted into Finagle services. type Endpoint[-A, +B] = Router[Microservcie[A, B]]
implicit def[A, B](e: Endpoint[A, B]): Service[A, B] = ??? Would love to hear any suggestions and comments. What I like more about this approach is that it doesnt't break anything (except for |
I'll try and spend some more time digging into this tomorrow, but I'm a bit worried about There's also the matter that I think is mentioned in #190 Composing routers with Finagle filters/services. The services that I have written are using Finagle Filters to decorate the incoming request and perform authorization before passing the authorized request onto an endpoint (ultimately made up of other endpoints). I like that the request readers have become more friendly to error messaging and the expressiveness has improved, but losing finagle filters is a deal breaker in my current use cases. I also don't know if anything has been done to allow for the pattern matching over combinators mentioned in #147 (comment) |
Thanks for the feedback Ben! Pattern matching over combinators is on this milestone #149. This approach doesn't prevent us from using filters. You can always implicitly convert |
Thanks for the link to #149. If we come up with a new name that makes sense it might be good to use that so that we can distance ourselves from the old name that is going to be deprecated. I'll try and make sure to carve out some time to see what it will take to update one of my projects (unfortunately I can't make any guarantees as the next few weeks are going to be very busy for me). |
I've only just seen it and I haven't put too much thought into it yet. But my initial gut feeling is that I'm not too fond of the I don't have any brilliant alternative idea yet, but I'd love to first think about what is really missing in the existing types for optimal composability. The main reason why I've put #184 on hold was that I did not know how to combine a custom request type with the result of a reader. If I have a filter/service that does Auth and then a reader, how can I have the "real" service deal with a domain object that contains the userId and the result from the reader? I'm not sure the
So, in short, I'd prefer to first define the gaps in composability that |
Ok, I thought about it a bit more and I'm afraid I would really recommend against introducing the
case class ~[A,B](a: A, b: B)
implicit class Foo2[A,B] (in: A ~ B) {
def ! = println("2")
}
implicit class Foo3[A,B,C] (in: A ~ B ~ C) {
def ! = println("3")
}
implicit class Foo4[A,B,C,D] (in: A ~ B ~ C ~ D) {
def ! = println("4")
}
val x: String ~ Int ~ String = new ~(new ~("foo", 2), "bar")
x ! //> 3 so some of the problems could potentially be solved with implicits without introducing a new public API. |
We don't have to solve this ticket in this milestone but we definitely must do it before 1.0.0. I wish we could solve this w/o introducing new API. @jenshalm could you please elaborate how the In my understanding the problem we're trying to solve is to allow users write domain-specific services rather than Let's say, we want to define a service that answers how many photos user has in the given album. The endpoint looks like val getNumberOfPhotos = new Service[Int ~ String ~ String, Int] {
...
} But in this case we will have to use pattern-matching to extract input arguments. We can avoid this defining a simple factory method object Microservice {
def apply[A, B, C, D](f: (A, B, C) => Future[D]) = new Service[A ~ B ~ C, D] {
def apply(req: A ~ B ~ C): Future[D] = req match {
case a ~ b ~ c => f(a, b, c)
}
}
} And then use is like this: val getNumberOfPhotos = Microservice { userId: Int, album: String, format: String =>
...
} |
The Ok, I'm clearer now about the goals. I'm still a bit confused about where the custom request types came from. Are we sure they are the right pattern? They seem to complicate things a bit in that they A) prevent implicit conversion of a reader to a service B) get lost when applied to a reader anyway. So there is always the extra challenge of passing the extra info from the custom request through. That's why I thought it might be more convenient to combine all parts that need a Request instead of chaining them, e.g. Note that I don't have the time to apply any sort of deeper thinking to it before the weekend, so some things I write might not make much sense. I just want to try to find the simplest solution (which is in line with finch philosophy), and the The |
Maybe we can build a mini-demo app for just the sample scenario you mentioned? Having a custom request carrying user info (coming from a filter?), a router extracting one param, a request reader extracting one param, a service function that only knows domain instances and a json encoder for the result. All wired with the existing 0.5.0 API as a fully working demo app and as a starting point to discuss improvements? |
I finally got what you mean! Instead of changing the request type with val auth: RequestReader[Int] = RequiredHeader("X-User-Id").map(_.toInt) So we can solve our counter-example as follows ( def getNumberOfPhotos(album: String)(userId: Int, format: String): Future[Int] = ???
val route: Router[RequestReader[Int]] = Get / "user" / "albums" / string /> { album =>
auth ~ RequiredParam("format") ~> getNumberOfPhotos(album)
} Yes. We can start with simple demo to have a sandbox for experiments. Although, I like the idea of using RR instead filters for authorization. I don't think that changing request type is a good pattern. Finagle-httpx doesn't even provide API for extending request (that's why we have |
Yes, that's kind of what I meant. The only downside I see is that the Where do you want to create the demo app/playground? It might be good to have the original version here in this repo so that users could create PRs to demonstrate different approaches for improved APIs. |
Yeah. I will create the single-file mini-demo project here as a sub-project. Probably it make sense to name it a |
Playground is on review #205. My plan for this weekend is to play with it and come up with PR for the Finch in Action problem. |
I want to merge #206 PR as an experimental feature of 0.6.0 release. The gist that describes experimental features is here: https://gist.github.com/vkostyukov/e0e952c28b87563b2383. Please let me know if you have any concerns. |
I'm really excited about the job you did there guys! We've started using finch in our project and made few abstractions over At first I wrote some implicit conversions from Then we've came with an idea to hide
Now we're able to do whatever we want with I've just read your gist about microservices and it reminds me my attempts to do the same thing - skip controller "proxy" functions and use straight service's methods. I tried to make an Hope you will do it! |
Thanks @imliar! That is super cool what are you doing with Finch. Hopefully, something similar will be shipped in 0.6.0 release. BTW, feel free to add your company into the adopters list. What you described first two paragraphs: |
There are still some important cases to think about. |
The typical use case is have a top-level filter that handles all the possible errors from the stack. All the different failures in The scenario is following: you write your micro-services that respond some |
This is solved in #206. |
This is a big ticket that related to #190, #172 and PR #184. For now we have amazing and highly-composable basic blocks but it's still not really clear how to use them together. One and simple example is shown in the demo project, although I was wondering if we could do better.
There is nothing wrong with using Finch in a straightforward way (via
Service[HttpRequest, HttpResponse]
). But we also should think about using it in a more convenient manner. The PR #184 is a good example of this effort. My hight-level idea is to provide users a way to write their microservices as functions:and then use Finch basic blocks to serve those functions on Finagle. It might look as follows:
We don't really need to convert request reader into service (per #184) or create an explicit service instance. We do have everything is needed in
RequestReader
so we can just map it:Thus, we may define an
Endpoint
astype Endpoint[+A] = Router[RequestReader[A]]
. Therefore, we're replacingService
withRequestReader
, which sounds reasonable for me:Req => Future[A]
RequestReader
s are composable as hell (we did a great job),Service
s are not (!
is an ugly hack).The text was updated successfully, but these errors were encountered: