Devreal

SF Scala at Twitter: Steve Cosenza, Introducing Finatra 2.0

SF Scala at Twitter: Steve Cosenza, Introducing Finatra 2.0

Recording: SF Scala at Twitter: Steve Cosenza, Introducing Finatra 2.0

[Music] my name is Steve cosenza the tech lead of the furniture and data API teams here at Twitter and tonight I'm going to be talking about some of the exciting new features of fenêtre version 2 so what is fanara furniture is a framework for easily building API services on top of Twitter Scala stack currently we support HTTP services and this you'll be familiar with if you've used fenêtre v1 but we are also planning to support services in the future first the brief project history fenêtre v1 was started as a github project in early 2012 by to Twitter engineers Chris Burnette who's here tonight and Julio kubota in the middle of 2014 myself and Chris coca working on the data API team released fenêtre version to internally at Twitter was a snapshot release beginning of this year from atrophy one had their final release this was supporting Scala 29 and Scala to 10 and then this talk ends up being well timed and that we just released the first milestone of furniture v22 maven central just about an hour ago and we have now merged [Laughter] thank you and we've also emerged the version 2 line of code into the master branch of an actress github page some highlights fanaka v2 is a complete rewrite from the ground up but we worked with the finish review on engineers to maintain the simple spirit that made fanaka v1 so popular in some internal benchmarks of let's say get requests returning JSON objects phenoxy ov2 was about 50 times faster than its predecessor v2 comes with powerful feature and integration testing support and i'll be talking about some of these details later in the presentation tomorrow v2 now supports jsr 330 dependency injection annotations for use throughout your service we've chosen to use google juice as our implementation of those annotations but it's worth noting that it's completely optional to use these annotations or juice at all and writing your furniture service however if you do choose to use them you'll find excellent support provided throughout we now integrate with jackson for seamless JSON parsing again this is optional but if you choose to use it you'll find some enhancements to the jackson scala module which includes support for required fields default values and also validations of fields the framework now uses slf4j for all framework clogging and optionally if you choose to use log back as your implementation we provide a integration with finagle futures that will let you do map diagnostic contract tributes and have them propagate across features furniture is now built on a library that we're also releasing called Twitter inject and this library provides the integration points of dependency injection with common finagle classes and Twitter server classes and you can see the names of the libraries up here and these are usable outside furniture as well furniture is now decomposed into several libraries the top of screen you'll see fenêtre HTTP this is the main dependency you would use to create a finisher HTTP service you'll also see our integrations with jackson log back HTTP client is kind of a bare-bones client built on top of finagle and finisher utils is a grab bag of utilities that we found useful when creating for nacho services okay so let's get right into it and see some concrete examples of finisher be too in action will create a simple HTTP API that lets you post tweets but first before we start writing some code let's create a feature test first so feature tests are a form of black box testing where we start a locally running server and issue actual HTTP requests and assert actual HTTP responses better returns in this way we test the dirt externally visible features of the API hence the name feature testing with the feature test the main class that we use is embedded HTTP server and we hand that a Twitter server in this case since we haven't created a Twitter server yet we just pass an anonymous HTTP server to it we then specify in the tests that we'd like to issue an HTTP post the path is / tweet the post body you can see here it's JSON representing a tweet and then we have a few assertions the first assertion is that we expect an HTTP 201 created response and we expect a location header to be that points at the newly created tweet so now let's run that test Oh before we run it let's do one additional assertion that's also a shirt that there'll be a JSON body returned back to us and in this case it's going to mirror the same JSON that was posted except there's an additional ID field the idea of the tweet that was created now let's run the test okay so as expected the test fails but let's look at the test output so you'll see a post was issued to slash tweet here are the headers that were sent note that we posted a content type of application JSON here's the post body and now everything between this equals and this minus line these are all log messages that the server's output while processing this request so in this case the routing service said I can't find any routes for posts to slash tweet and then finally down here is what the service actually returned to us a return to not found with an empty body so in order to get this test passing now we can create a controller controllers extend the controller class and they follow the Sinatra style HTTP method route definition and then a call back to handle a request in this case we directly take in a finagle HTTP request as our input and we use a response builder that's provided to us by the controller to create a response so in this case will return a HTTP 201 created response and a body of hello also note that this callback is not returning a future of course if you're calling a blocking API you definitely want to keep that future throughout but in this case if nothing is blocking you could simply return the response directly and finagle or finocchiaro will take care of converting it into a future okay so now that we have a controller we can now create a server servers now extend the HTTP server class you then override a configure a chibi method it hands you an HTTP or router and you can now add things such as filters or controllers to this router in this example we're demonstrating qap is the first is an API where you could pass a type directly to the router and then furniture is in charge of creating that type for you and in the bottom case we knew up our controller ourselves and then this API you pass in the instance and it uses it directly and a little bit later in the presentation I'll show an example where we use a type for the controller now that we have a real controller in a real server we add our real server to our feature test fan rigger on it and now you could see the test is failing but for a different reason this case it's saying an unrecognized token of hello was found this is because we specified a JSON body was the expected response and hello is not valid isn't also notice at the bottom will print both the received response bodies and the expected response bodies on single lines and this little asterisks that tells us the point at which those lines diverged so in this example it's not that useful but you can imagine if you had a single character that was different that asterisks can help you okay so in our continuing effort to get this test passing let's now parse some JSON and generate some real JSON so as I mentioned earlier we provide an integration with Jackson if you choose to use it the basic premise is that you create a case class that mimics the JSON that you want to parse so at the top of the screen you could see the JSON representing a tweet and over here you could see we created a case class called posted tweet that has the same field names as the JSON worth noting is that latitude in this case is posted to us as a JSON string but in the location object it's a double so Jackson will take care of doing all the value conversions for you also worth mentioning is that since we now support required verse optional fields if message is not included since it's not an option that is a validation error where Jackson normally would return a null in that field if you do have an optional field let's say location is emitted in the JSON a nun value will end up in your case class and then for the sensitive field if that's not in your JSON the default value of false will be used also worth noting furniture comes with a default Jackson configuration but this is completely customizable now that we have a case class to represent our posted tweet we could update our controllers call back notice now instead of this being a finagle HTTP request it's our case class directly and finisher will take care of posting the request into this object will also for now just directly return that posted tweet and our created response body rerun the tests and now you see it fails because the ID is missing so by returning that case class for not returned that case glass into JSON it was parsed as JSON the JSON diff failed and we're now missing the ID okay so let's generate some JSON now to generate JSON it's the same pattern as parsing JSON create a case class to represent the JSON you'd like to generate it's worth noting that you can use the same case glass to parse use throughout your services and also produce your JSON but we found that if you separate this concern you get a lot more flexibility and being able to refactor your domain objects but maintain a backwards compatible API and what you're parsing and what you're generating so in this case will create a serialized tweet case class looks the same as the other one except now we add an ID field and notice that ID field it's a status ID type that status ID it's a case class that extends this wrapped value trait and what this does is signal to Jackson that I really want you to treat this as the wrapped string inside of it so I want you to parse it and generate it just as that string but now within your application you could pass around a status ID instead of just a nun type string okay so now we'll update the controller to use this new tweet and what we'll do now is we'll just create a hard-coded status ID with 123 let's assume for this example that we already have a status case class it's the main kind of domain object that we use throughout services at Twitter will take the posted tweet turn that into our domain object passing in the status ID get a status out here's where we'll be saving or status ID in a future slide when we're done we create a render will tweet from that domain object and then we put that in our created response body so rerun that test and yeah it succeeds and as you can see here I created was returned with the JSON body we expected okay so at this point let's take a brief break from that main feature test to write some additional feature tests that test some edge cases and different types of JSON that may be posted to us so the first edge case will be lets say message field is not specified in the JSON run that test and you'll see it immediately succeeds we get a 400 bad request returns and there is a response body with some json that indicates the message field is a required field so this type of error response it's the default error that you'll get at a furniture but it's completely customizable let's rural test the test for some invalid values in this case we have a message field but it's empty and for latitude we specify a value that's not valid we run that test and this test fails it fails because we expected a bad request but it created a tweet first anyway and it used these invalid values so if we want furniture to be able to return a bad request in this case we have to add some additional hints that indicate what type of values are valid and for this we use a new furniture v2 feature called validation annotations out-of-the-box furniture comes with the following validation annotations and it's easy to create your own so let's update our case class to use these validation annotations top of the screen will add the at size validation to our message string and specify that it has to be at least one character but a max of 140 and then for our location will use the at range validation to specify a valid range for latitude and longitude so you could think of at size as being used for types that would have sizes so things like strings or sequences a man at range for being used for ranges of values maybe no I believe actually these are valid values yes all right so now with these validation annotations rerun the tests and you'll see it succeeds note now in our air response we indicate both validation errors both that message size is not between our expected and the latitude is not between our expected so we collect all the different validation errors and we don't fail fast so now let's go back to our original feature test and try to actually save this tweet off and to do this we're going to use a cloud based database named fire vez so we're going to create a class that will let us save a tweet and this class is going to list the help of two other classes the ID service which will allow us to ask for the next tweet ID and a firebase client that will let us talk to our database and to gain access to these dependencies will use the jsr 330 dependency injection annotation called at inject so by putting at inject on this classes constructor we're signaling tufa not sure that when you create this service for us provide us these dependences will also add an at singleton to the service class and that signals that we only want a single service to be created for the entire furniture server now it's worth noting at this point that the jsr 330 annotations they're available in a standalone jar it's very small and it only specifies a few annotations in a few interfaces so if someone does not want to use a JSR 330 compatible at runtime to create this class they're free to new it up themselves and pass in those dependencies in the constructor so you could think of this as being a nice unobtrusive way of declaring your dependencies and then if you'd like the framework for not sure to use them you can alright so now similar to the tweet service will inject the tweet service into our controller so same pattern you added at inject cue the controllers constructor and we'll also add at singleton indicating we only want a single controller / fenêtre server will then update our server at the top of the screen you could see the server before where we manually created our controller and pass it into the router and on the bottom of the screen you'll see we now pass in the controller by type and this signals to finisher you create it and if there are at inject annotations on it also take care to create those classes and pass them in you'll also notice that we provide a sequence of modules these are juice modules that if needed can provide additional configuration which is needed to create some of the classes that you need so in this case we have to provide a module that says how to create a firebase client so here's an example of what that firebase client looks like and the reason why we need this client is we have to specify where the actual firebase external service is located so we'll extend an HTTP client module this is provided in the furniture HTTP client library and you could then specify a destination and a retry policy and destination this is a finagle destination value we say we'd like you to use the flag resolver so this will allow you to specify where firebase is located in a finagle flag okay so well now going to rerun our original test it's on screen too and now this test that was previously passing it now fails the reason it now fails is when it tried to do the put to firebase it failed and said no hosts are available for flag fire vez and the reason is we didn't specify a location for fire miss however since we want this feature test to be able to be run locally we want it to be deterministic you want to be able to run it in CI we're going to need to mock out that firebase client call so here's how we do that so at the top of the screen here's our feature test before and at the bottom of the screen we now use a fanara trait called feature test by using this feature test trait we make it easy to override classes in the server with things like let's say mocks so in this example by including this feature test and adding this module to our server we can then at bind different values so we could say the firebase client that your server wants we want you to use a smart mock created by makito and then we want the server to use that similarly create a smart mock for the ID service and then bind that into the server so we now have a locally running server where we're able to selectively swap out classes in a test here is now that test with the at vines now in our post tweet we'll use the mock ID service and we'll stub out the X the desired response so in this case when Kennedy is called return a future with a status ID of one to three and when firebase client put is called to this path with a status object that we expect return a successful future now we could rerun this feature dust and it will now pass so we now have a test of the locally running server its end to end and it relies on zero external dependencies okay I'd like to introduce one additional testing type and this is called a startup test start of tests look pretty similar to feature tests but their goals are slightly different and that you want to avoid override modules or mocks you basically want your service to be as close to production as possible and what this allows us to do is in a test verify that our entire object graph is correct and fully specified you'll notice we now specify stage equals production this signals to furniture and juice to eagerly create the entire object graph at startup time this is what we do in production but normally in feature tests we lazily create these well also you'll notice we're passing in a command line flag and we're passing in the Twitter server resolver map flag and as the value of vet flag we say hey firebase we want you to be equal to the nil resolver and the nil resolver is essentially an empty list of hosts and by specifying this empty list of hosts we now allow the server to start up completely without having to connect to any external systems our tests then says server assert that you're healthy this will pull the server wait for it to completely start up check its health endpoint that's provided to us from Twitter server and make sure that everything came up completely at the end of the day we've found great value in using startup tests with juice and creating our services and the way to think about it is juice provides lots of modularity benefits and testing benefits but it comes at the cost of full compile-time safety however with a start-up fest we can mitigate lots of this cost you don't get the error immediately a compile-time but as soon as you're running your test suite you immediately see that error it tells you what's wrong and kind of by including both of these we feel you get the best of both worlds okay I'm going to briefly go through some additional v2 features we support message body readers and writers these allow you to deal with just your domain object as the input of your route and the output of your out but it'll take care of in this example taking your status object and turning that into the seer and last week we fully support the clarity of request parsing so this is really similar to how we parse JSON you could also use this for things like get requests so in the top of the example you'll see we create a custom case class the route / M of ID will be parsed into a status ID object and expand query / am will be parsed into a boolean you could do all the validations that it showed in a previous slide all the type conversions will be done for you we support required verse optional and we also support default values if you're using Twitter util flags we have an at flag annotation that lets you inject the value of those flags anywhere throughout your application we fully support mustache templates we now have a at mustache annotation you could throw it on a case class return that case class from your controller and then the name of your mustache template it'll be rounded for that template and serialize that we have support for two types of server warm up the first if you set that flag to true your server will not start accepting incoming requests until all finagle clients have been resolved so this is really important if you're connecting let's say to a back-end service there's a few hundred or a few thousand nodes takes a while to do that resolution and then we also have a warm up method override that warm up method at any application specific warm up object exercise different controller paths different services and give the JVM time to warm up and finally I wanted to show that feature tests are not limited to a single server you can bring up multiple servers especially if the servers have dependencies on each other so in this example we have an HTTP server that calls a back-end thrift server we could create both in the feature test tell the HTTP server with a resolver map when you try to resolve the echo service point at this thrift servers external point and then both of these servers will be wired together you could run a future test that runs through both of them so that's all I have for tonight I'd like to thank the furniture team that contributed to finish or v2 on screen and I'll leave you with the server and controller from the example and open it up to questions [Applause] in not your to do you guys support automatically registering finagle stats to all the routes and things that are defined and the methods so you'll notice the common filters one of those filters will give you finagle stats for all requests our responses and stuff like that we don't currently support / route stats but that is something that's on our backlog but you are free now to create a filter that would do that but yeah great question do you have any experience or or good case studies where you used this or previous version to Clemente hypermedia api's for you know marshaling Jackson into maybe a higher level object that has Lincoln and other kinds of descriptive metadata yeah great question we do not directly have experience with that with fanara but using something like a message body writer would allow you to return kind of any type of object you like and then see realize that as you wish but you have no direct support but certainly could be something that could be added I I wanted to say cool this is awesome but a another question I had when you were newing up embedded HTTP server yeah I believe the actual parameter name was Twitter server yes seems sort of unfortunate yes as a anyway so yeah great question the reason for that is you actually can specify any Twitter server in that location so let's say you want to be a Twitter server you want to directly use finagle to create your HTTP server or even let's say use something like Finch you can still use this test class to start up the server locally and then issue requests and responses yeah good catch though I guess my question how about web sockets or is that something great question we do not currently add any additional support on top of finagle for web sockets but as we start to use speedy and HTTP two more I would expect us to see some of those features I have any support for swagger that is a great question we currently have a experimental directory in house where we actually had a proof of concept where we generated a swagger you I based on the controllers and routes so when you're doing the declarative parsing it's actually really great to implement something like that because you're fully specifying kind of all the query params route params what the types are so it's very doable but yes not currently completely working but that is something that yet the community is interested in we could get that out there and we would definitely take that as a contribution sard in here so we have many production servers that currently run on a v2 and let's see I guess future versions future services are coming out and they are using fenêtre v2 so thanks thanks [Applause] [Music]