SlideShare une entreprise Scribd logo
1  sur  47
Télécharger pour lire hors ligne
Finatra v2
Fast, testable Scala services
Steve Cosenza @scosenza
Finatra & Data API Tech Lead @twitter
What is Finatra?
• Finatra is a framework for easily building API services on top of
Twitter’s Scala stack (twitter-server, finagle, twitter-util)
• Currently supports building HTTP services
• Plan to support building Thrift services
Project History
• Feb 2012: Finatra project started on Github by two Twitter engineers
(Julio Capote and Chris Burnett)
• June 2014: Christopher Coco and Steve Cosenza on the Data API
team release Finatra v2.0.0-SNAPSHOT internally at Twitter
• Jan 2015: Finatra v1.6 released for Scala 2.9 and 2.10
• April 2015: Finatra v2.0.0M1 publicly released for Scala 2.10 and 2.11
Highlights
• Production use as Twitter’s HTTP framework
• ~50 times faster than v1.6 in several benchmarks
• Powerful feature and integration test support
• JSR-330 Dependency Injection using Google Guice
• Jackson based JSON parsing supporting required fields, default values,
and custom validations
• Logback MDC integration for contextual logging across Futures
• Guice request scope integration with Futures
Twitter Inject Libraries
• inject-core
• inject-app
• inject-server
• inject-modules
• inject-thrift-client
• inject-request-scope
Finatra Libraries
• finatra-http
• finatra-jackson
• finatra-logback
• finatra-httpclient
• finatra-utils
Tutorial
Let’s create a simple HTTP API for Tweets :-)
Create a FeatureTest
class TwitterCloneFeatureTest extends Test {


val server = new EmbeddedHttpServer(

twitterServer = new HttpServer {})



"Post tweet" in {

server.httpPost(

path = "/tweet",

postBody = """

{

"message": "Hello #SFScala",

"location": {

"lat": "37.7821120598956",

"long": "-122.400612831116"

},

"sensitive": false

}”"",
andExpect = Created,
withLocationHeader = "/tweet/123")

}

}
And let’s assert the response body
"Post tweet" in {

server.httpPost(

path = "/tweet",

postBody = """

{

"message": "Hello #SFScala",

"location": {...}
"sensitive": false

}”"",

andExpect = Created,

withLocationHeader = "/tweet/123",

withJsonBody = """

{

"id": "123",

"message": "Hello #SFScala",

"location": {

"lat": "37.7821120598956",

"long": "-122.400612831116"

},

"sensitive": false

}""")

}
Test Failure: Route not found
===========================================================================
HTTP POST /tweet
[Header] Content-Length -> 204
[Header] Content-Type -> application/json;charset=utf-8
[Header] Host -> 127.0.0.1:60102
{
"message" : "Hello #SFScala",
"location" : {
"lat" : "37.7821120598956",
"long" : "-122.400612831116"
},
"sensitive" : false
}
===========================================================================
RoutingService Request("POST /tweet", from /127.0.0.1:60103) not found in
registered routes:
---------------------------------------------------------------------------
[Status] 404 Not Found
[Header] Content-Length -> 0
*EmptyBody*
Create a Controller
package finatra.quickstart.controllers



import com.twitter.finagle.http.Request

import com.twitter.finatra.http.Controller



class TweetsController extends Controller {



post("/tweet") { request: Request =>

response.created("hello")

}

}
Create a Server
import com.twitter.finatra.HttpServer

import com.twitter.finatra.filters.CommonFilters

import com.twitter.finatra.routing.HttpRouter

import finatra.quickstart.controllers.TweetsControllerV1



class TwitterCloneServer extends HttpServer {



override def configureHttp(router: HttpRouter): Unit = {

router

.filter[CommonFilters]

.add(new TweetsController)

}

}
Add Server to FeatureTest
class TwitterCloneFeatureTest extends Test {


val server = new EmbeddedHttpServer(

twitterServer = new TwitterCloneServer)



...
}
Test Failure: Unexpected response body
===========================================================================
HTTP POST /tweet
[Header] Content-Length -> 204
[Header] Content-Type -> application/json;charset=utf-8
[Header] Host -> 127.0.0.1:60118
...
===========================================================================
127.0.0.1 - - [24/Apr/2015:21:36:25 +0000] "POST /tweet HTTP/1.1" 201 5 9 "-"
---------------------------------------------------------------------------
[Status] 201 Created
[Header] Content-Length -> 5
hello
Unrecognized token 'hello': was expecting ('true', 'false' or 'null')
at [Source: hello; line: 1, column: 11]
JSON DIFF FAILED!
*
Received: hello
Expected: {“id":"123","location":{"lat":"37.7821120598956","long":"-122.4006 ...
Let’s parse some JSON
{

"message": "Hello #SFScala",

"location": {

"lat": "37.7821120598956",

"long": "-122.400612831116"

},

"sensitive": false

}
case class PostedTweet(

message: String,

location: Option[Location],

sensitive: Boolean = false) {



def toStatus(id: StatusId) = { ... }

}
case class Location(

lat: Double,

long: Double)

Update Controller to use PostedTweet
class TweetsController extends Controller {



post("/tweet") { postedTweet: PostedTweet =>

response.created(postedTweet)

}

}

Test failure: ’id’ field in body missing
[Status] 201 Created
[Header] Content-Type -> application/json; charset=utf-8
[Header] Content-Length -> 107
{
"message" : "Hello #SFScala",
"location" : {
"lat" : 37.7821120598956,
"long" : -122.400612831116
},
"sensitive" : false
}
JSON DIFF FAILED!
*
Received: {“location”:{"lat":37.7821120598956,"long":-122.400612831116}, ...
Expected: {“id”:”123","location":{"lat":"37.7821120598956","long":"-122. ...
Let’s generate some JSON
{

"id": 123,

"message": "Hello #SFScala",

"location": {

"lat": "37.7821120598956",

"long": "-122.400612831116"

},

"sensitive": false

}
case class RenderableTweet(

id: StatusId,

message: String,

location: Option[RenderableLocation],

sensitive: Boolean)

case class StatusId(

id: String)

extends WrappedValue[String]
case class RenderableLocation(

lat: String,

long: String)
Update Controller to use RenderableTweet
class TweetsController extends Controller {



post("/tweet") { postedTweet: PostedTweet =>

val statusId = StatusId("123")

val status = postedTweet.toDomain(statusId)
// Save status here


val renderableTweet = RenderableTweet.fromDomain(status)

response

.created(renderableTweet)

.location(renderableTweet.id)

}

}

Feature Test Success!
===========================================================================
HTTP POST /tweet
...
===========================================================================
127.0.0.1 - - [24/Apr/2015:22:38:31 +0000] "POST /tweet HTTP/1.1" 201 122 642 "-"
---------------------------------------------------------------------------
[Status] 201 Created
[Header] Content-Type -> application/json; charset=utf-8
[Header] Location -> http://127.0.0.1:60352/tweet/123
[Header] Content-Length -> 122
{
"id" : "123",
"message" : "Hello #SFScala",
"location" : {
"lat" : "37.7821120598956",
"long" : "-122.400612831116"
},
"sensitive" : false
}
Let’s test that ‘message’ is required
"Missing message field" in {

server.httpPost(

path = "/tweet",

postBody = """

{

"location": {

"lat": "37.7821120598956",

"long": "-122.400612831116"

},

"sensitive": false

}""",

andExpect = BadRequest)

}
Test Success
===========================================================================
127.0.0.1 - - [24/Apr/2015:22:57:56 +0000] "POST /tweet HTTP/1.1" 400 42 570 "-"
---------------------------------------------------------------------------
[Status] 400 Bad Request
[Header] Content-Type -> application/json;charset=utf-8
[Header] Content-Length -> 42
{
"errors" : [
"message is a required field"
]
}
Let’s test for invalid values
"Invalid fields" in {

server.httpPost(

path = "/tweet",

postBody = """

{

"message": "",

"location": {

"lat": "9999",

"long": "-122.400612831116"

},

"sensitive": false

}""",

andExpect = BadRequest)

}
Test Failed (expected 400 Bad Request)
===========================================================================
127.0.0.1 - - [25/Apr/2015:01:58:27 +0000] "POST /tweet HTTP/1.1" 201 98 742 "-"
---------------------------------------------------------------------------
[Status] 201 Created
[Header] Content-Type -> application/json; charset=utf-8
[Header] Location -> http://127.0.0.1:49189/tweet/123
[Header] Content-Length -> 98
{
"id" : "123",
"message" : "",
"location" : {
"lat" : "9999.0",
"long" : "-122.400612831116"
},
"sensitive" : false
}
201 Created did not equal 400 Bad Request
Validation Annotations
• CountryCode
• FutureTime
• PastTime
• Max
• Min
• NonEmpty
• OneOf
• Range
• Size
• TimeGranularity
• UUID
• MethodValidation
Let’s add some validation annotations
case class PostedTweet(

@Size(min = 1, max = 140) message: String,

location: Option[Location],

sensitive: Boolean = false) {
case class Location(

@Range(min = -90, max = 90) lat: Double,

@Range(min = -180, max = 180) long: Double)

Test Success
===========================================================================
127.0.0.1 - - [24/Apr/2015:23:01:10 +0000] "POST /tweet HTTP/1.1" 400 106 660 "-"
---------------------------------------------------------------------------
[Status] 400 Bad Request
[Header] Content-Type -> application/json;charset=utf-8
[Header] Content-Length -> 106
{
"errors" : [
"message size [0] is not between 1 and 140",
"location.lat [9999.0] is not between -90 and 90"
]
}
Next let’s write a class to save a tweet
@Singleton

class TweetsService @Inject()(

idService: IdService,

firebase: FirebaseClient) {



def save(postedTweet: PostedTweet): Future[Status] = {

for {

id <- idService.getId()

status = postedTweet.toStatus(id)
path = s"/statuses/${status.id}.json"

_ <- firebase.put(path, status)

} yield status

}

}
Inject TweetsService into Controller
@Singleton

class TweetsController @Inject()(

tweetsService: TweetsService)

extends Controller {



post("/tweet") { postedTweet: PostedTweet =>

tweetsService.save(postedTweet) map { status =>
val renderableTweet = RenderableTweet.fromDomain(status)

response

.created(renderableTweet)

.location(renderableTweet.id)

}

}

}
Update Server
//Before
class TwitterCloneServer extends HttpServer {



override def configureHttp(router: HttpRouter): Unit = {

router

.filter[CommonFilters]

.add(new TweetsController)

}

}
//After
class TwitterCloneServer extends HttpServer {



override def modules = Seq(FirebaseHttpClientModule)



override def configureHttp(router: HttpRouter): Unit = {

router

.filter[CommonFilters]

.add[TweetsController]

}

}
FirebaseHttpClientModule
object FirebaseHttpClientModule extends HttpClientModule {



override val dest = "flag!firebase"



override def retryPolicy = Some(exponentialRetry(

start = 10.millis,

multiplier = 2,

numRetries = 3,

shouldRetry = Http4xxOr5xxResponses))

}

Rerun ‘Post tweet’ Test
"Post tweet" in {

server.httpPost(

path = "/tweet",

postBody = ...,

andExpect = Created,

withLocationHeader = "/tweet/123",

withJsonBody = """

{

"id": "123",

"message": "Hello #SFScala",

"location": {

"lat": "37.7821120598956",

"long": "-122.400612831116"

},

"sensitive": false

}""")

}
Test Fails: “no hosts are available”
===========================================================================
HTTP POST /tweet
{
"message" : "Hello #SFScala",
...
}
===========================================================================
Request("PUT /statuses/374ea2dd-846a-45a5-a296-bcf43e5830c3.json”)
service unavailable com.twitter.finagle.NoBrokersAvailableException:
No hosts are available for flag!firebase
127.0.0.1 - - [25/Apr/2015:02:37:01 +0000] "POST /tweet HTTP/1.1" 503 34 584 "-"
---------------------------------------------------------------------------
[Status] 503 Service Unavailable
[Header] Content-Type -> application/json;charset=utf-8
[Header] Content-Length -> 34
{
"errors" : [
"service unavailable"
]
}
Let’s mock our services
// Before

class TwitterCloneFeatureTest extends Test {

val server = new EmbeddedHttpServer(

twitterServer = new TwitterCloneServer)



"Post tweet" in { ... }
}
// After

class TwitterCloneFeatureTest extends FeatureTest with Mockito {



override val server = new EmbeddedHttpServer(

twitterServer = new TwitterCloneServer {

override val overrideModules = Seq(integrationTestModule)

})



@Bind val firebaseClient = smartMock[FirebaseClient]



@Bind val idService = smartMock[IdService]



“Post tweet" in { ... }
}
Let’s mock our services
class TwitterCloneFeatureTest extends FeatureTest with Mockito {



override val server = new EmbeddedHttpServer(

twitterServer = new TwitterCloneServer {

override val overrideModules = Seq(integrationTestModule)

})



@Bind val firebaseClient = smartMock[FirebaseClient]



@Bind val idService = smartMock[IdService]



“Post tweet" in {
val statusId = StatusId("123")

idService.getId returns Future(statusId)



val putStatus = Status(id = statusId, ...)

firebaseClient.put("/statuses/123.json", putStatus) returns Future.Unit
val result = server.httpPost(

path = "/tweet",
...)

Feature Test Success!
===========================================================================
HTTP POST /tweet
...
===========================================================================
127.0.0.1 - - [24/Apr/2015:22:38:31 +0000] "POST /tweet HTTP/1.1" 201 122 642 "-"
---------------------------------------------------------------------------
[Status] 201 Created
[Header] Content-Type -> application/json; charset=utf-8
[Header] Location -> http://127.0.0.1:60352/tweet/123
[Header] Content-Length -> 122
{
"id" : "123",
"message" : "Hello #SFScala",
"location" : {
"lat" : "37.7821120598956",
"long" : "-122.400612831116"
},
"sensitive" : false
}
Startup Test
class TwitterCloneStartupTest extends Test {



val server = new EmbeddedHttpServer(

stage = PRODUCTION,

twitterServer = new TwitterCloneServer,

clientFlags = Map(

"com.twitter.server.resolverMap" -> "firebase=nil!"))



"server" in {

server.assertHealthy()

}

}
Guice with Startup Tests
• Guice provides significant modularity and testing benefits
• Startup tests mostly mitigate lack of compile time safety
• We’ve found the combination to be a good compromise
Additional v2 Features
• Message body readers and writers
• Declarative request parsing
• Injectable flags
• Mustache Templates
• Multi-Server Tests
• Server Warmup
Message body readers and writers
class StatusMessageBodyWriter extends MessageBodyWriter[Status] {



override def write(status: Status): WriterResponse = {

WriterResponse(

MediaType.JSON_UTF_8,

RenderableTweet.fromDomain(status))

}

}
Declarative request parsing
case class GetTweetRequest(

@RouteParam id: StatusId,

@QueryParam expand: Boolean = false)
case class GetTweetsRequest(

@Range(min = 1, max = 100) @QueryParam count: Int = 50)
@Singleton

class TweetsController extends Controller {


get(“/tweet/:id”) { request: GetTweetRequest =>

...
}
get(“/tweets/?”) { request: GetTweetsRequest =>

...
}

}
Injectable flags
@Singleton

class TweetsService @Inject()(

@Flag("max.count") maxCount: Int,

idService: IdService,

firebase: FirebaseClient) {
...
}
Mustache templates
// user.mustache
id:{{id}}
name:{{name}}

@Mustache("user")

case class UserView(

id: Int,

name: String)

get("/") { request: Request =>

UserView(

123,

"Bob")

}
Server Warmup
class TwitterCloneServer extends HttpServer {

override val resolveFinagleClientsOnStartup = true



override def warmup() {

run[TwitterCloneWarmup]()

}
...

}
Multi-Server tests
class EchoHttpServerFeatureTest extends Test {



val thriftServer = new EmbeddedThriftServer(

twitterServer = new EchoThriftServer)



val httpServer = new EmbeddedHttpServer(

twitterServer = new EchoHttpServer,

clientFlags = Map(

resolverMap(“flag!echo-service“ -> thriftServer.thriftExternalPort)))



"EchoHttpServer" should {

"Echo msg" in {

httpServer.httpGet(

path = "/echo?msg=Bob",

andExpect = Ok,

withBody = "Bob")

}

}

}

Finatra Contributors
• Steve Cosenza
• Christopher Coco
• Jason Carey
• Eugene Ma
Questions?
class TwitterCloneServer extends HttpServer {

override val resolveFinagleClientsOnStartup = true

override val modules = Seq(FirebaseHttpClientModule)

override def warmup { run[TwitterCloneWarmup]() }
override def configureHttp(router: HttpRouter): Unit = {

router

.register[StatusMessageBodyWriter]

.filter[CommonFilters]

.add[TweetsController]

}

}
@Singleton

class TweetsController @Inject()(

tweetsService: TweetsService)

extends Controller {

post("/tweet") { postedTweet: PostedTweet =>

tweetsService.save(postedTweet) map { status =>

response

.created(status)

.location(status.id)

}

}

}

Contenu connexe

Tendances

Tendances (20)

Autoscaling with hashi_corp_nomad
Autoscaling with hashi_corp_nomadAutoscaling with hashi_corp_nomad
Autoscaling with hashi_corp_nomad
 
Introducing Middy, Node.js middleware engine for AWS Lambda (FrontConf Munich...
Introducing Middy, Node.js middleware engine for AWS Lambda (FrontConf Munich...Introducing Middy, Node.js middleware engine for AWS Lambda (FrontConf Munich...
Introducing Middy, Node.js middleware engine for AWS Lambda (FrontConf Munich...
 
VCL template abstraction model and automated deployments to Fastly
VCL template abstraction model and automated deployments to FastlyVCL template abstraction model and automated deployments to Fastly
VCL template abstraction model and automated deployments to Fastly
 
Distributed Eventing in OSGi
Distributed Eventing in OSGiDistributed Eventing in OSGi
Distributed Eventing in OSGi
 
Composable and streamable Play apps
Composable and streamable Play appsComposable and streamable Play apps
Composable and streamable Play apps
 
Java Play RESTful ebean
Java Play RESTful ebeanJava Play RESTful ebean
Java Play RESTful ebean
 
Java Play Restful JPA
Java Play Restful JPAJava Play Restful JPA
Java Play Restful JPA
 
Serverless, The Middy Way - Workshop
Serverless, The Middy Way - WorkshopServerless, The Middy Way - Workshop
Serverless, The Middy Way - Workshop
 
Middy.js - A powerful Node.js middleware framework for your lambdas​
Middy.js - A powerful Node.js middleware framework for your lambdas​ Middy.js - A powerful Node.js middleware framework for your lambdas​
Middy.js - A powerful Node.js middleware framework for your lambdas​
 
Observability with Consul Connect
Observability with Consul ConnectObservability with Consul Connect
Observability with Consul Connect
 
Advanced VCL: how to use restart
Advanced VCL: how to use restartAdvanced VCL: how to use restart
Advanced VCL: how to use restart
 
Integrating icinga2 and the HashiCorp suite
Integrating icinga2 and the HashiCorp suiteIntegrating icinga2 and the HashiCorp suite
Integrating icinga2 and the HashiCorp suite
 
Get Real: Adventures in realtime web apps
Get Real: Adventures in realtime web appsGet Real: Adventures in realtime web apps
Get Real: Adventures in realtime web apps
 
Pycon - Python for ethical hackers
Pycon - Python for ethical hackers Pycon - Python for ethical hackers
Pycon - Python for ethical hackers
 
Static Typing in Vault
Static Typing in VaultStatic Typing in Vault
Static Typing in Vault
 
{{more}} Kibana4
{{more}} Kibana4{{more}} Kibana4
{{more}} Kibana4
 
Rack Middleware
Rack MiddlewareRack Middleware
Rack Middleware
 
Java9 Beyond Modularity - Java 9 más allá de la modularidad
Java9 Beyond Modularity - Java 9 más allá de la modularidadJava9 Beyond Modularity - Java 9 más allá de la modularidad
Java9 Beyond Modularity - Java 9 más allá de la modularidad
 
8 Minutes On Rack
8 Minutes On Rack8 Minutes On Rack
8 Minutes On Rack
 
Rhebok, High Performance Rack Handler / Rubykaigi 2015
Rhebok, High Performance Rack Handler / Rubykaigi 2015Rhebok, High Performance Rack Handler / Rubykaigi 2015
Rhebok, High Performance Rack Handler / Rubykaigi 2015
 

En vedette

En vedette (7)

Async Microservices with Twitter's Finagle
Async Microservices with Twitter's FinagleAsync Microservices with Twitter's Finagle
Async Microservices with Twitter's Finagle
 
Akka programação concorrente
Akka   programação concorrenteAkka   programação concorrente
Akka programação concorrente
 
Scalaの現状と課題
Scalaの現状と課題Scalaの現状と課題
Scalaの現状と課題
 
08.11.14 Impact Call Slides
08.11.14 Impact Call Slides08.11.14 Impact Call Slides
08.11.14 Impact Call Slides
 
Finch + Finagle OAuth2
Finch + Finagle OAuth2Finch + Finagle OAuth2
Finch + Finagle OAuth2
 
Finch.io - Purely Functional REST API with Finagle
Finch.io - Purely Functional REST API with FinagleFinch.io - Purely Functional REST API with Finagle
Finch.io - Purely Functional REST API with Finagle
 
Understanding Akka Streams, Back Pressure, and Asynchronous Architectures
Understanding Akka Streams, Back Pressure, and Asynchronous ArchitecturesUnderstanding Akka Streams, Back Pressure, and Asynchronous Architectures
Understanding Akka Streams, Back Pressure, and Asynchronous Architectures
 

Similaire à Finatra v2

GitBucket: The perfect Github clone by Scala
GitBucket: The perfect Github clone by ScalaGitBucket: The perfect Github clone by Scala
GitBucket: The perfect Github clone by Scala
takezoe
 

Similaire à Finatra v2 (20)

Taming WebSocket with Scarlet
Taming WebSocket with ScarletTaming WebSocket with Scarlet
Taming WebSocket with Scarlet
 
Workshop KrakYourNet2016 - Web applications hacking Ruby on Rails example
Workshop KrakYourNet2016 - Web applications hacking Ruby on Rails example Workshop KrakYourNet2016 - Web applications hacking Ruby on Rails example
Workshop KrakYourNet2016 - Web applications hacking Ruby on Rails example
 
Streaming twitter data using kafka
Streaming twitter data using kafkaStreaming twitter data using kafka
Streaming twitter data using kafka
 
RoR Workshop - Web applications hacking - Ruby on Rails example
RoR Workshop - Web applications hacking - Ruby on Rails exampleRoR Workshop - Web applications hacking - Ruby on Rails example
RoR Workshop - Web applications hacking - Ruby on Rails example
 
10 Excellent Ways to Secure Spring Boot Applications - Okta Webinar 2020
10 Excellent Ways to Secure Spring Boot Applications - Okta Webinar 202010 Excellent Ways to Secure Spring Boot Applications - Okta Webinar 2020
10 Excellent Ways to Secure Spring Boot Applications - Okta Webinar 2020
 
BEST REST in OpenStack
BEST REST in OpenStackBEST REST in OpenStack
BEST REST in OpenStack
 
Efficient HTTP Apis
Efficient HTTP ApisEfficient HTTP Apis
Efficient HTTP Apis
 
twMVC#46 一探 C# 11 與 .NET 7 的神奇
twMVC#46 一探 C# 11 與 .NET 7 的神奇twMVC#46 一探 C# 11 與 .NET 7 的神奇
twMVC#46 一探 C# 11 與 .NET 7 的神奇
 
Java Servlets.pdf
Java Servlets.pdfJava Servlets.pdf
Java Servlets.pdf
 
SharePoint Fest Chicago 2015 - Anatomy of configuring provider hosted add-in...
SharePoint Fest Chicago 2015  - Anatomy of configuring provider hosted add-in...SharePoint Fest Chicago 2015  - Anatomy of configuring provider hosted add-in...
SharePoint Fest Chicago 2015 - Anatomy of configuring provider hosted add-in...
 
OpenStack API's and WSGI
OpenStack API's and WSGIOpenStack API's and WSGI
OpenStack API's and WSGI
 
GitBucket: The perfect Github clone by Scala
GitBucket: The perfect Github clone by ScalaGitBucket: The perfect Github clone by Scala
GitBucket: The perfect Github clone by Scala
 
WCF - In a Week
WCF - In a WeekWCF - In a Week
WCF - In a Week
 
Solving anything in VCL
Solving anything in VCLSolving anything in VCL
Solving anything in VCL
 
Spring MVC to iOS and the REST
Spring MVC to iOS and the RESTSpring MVC to iOS and the REST
Spring MVC to iOS and the REST
 
[NDC 2019] Enterprise-Grade Serverless
[NDC 2019] Enterprise-Grade Serverless[NDC 2019] Enterprise-Grade Serverless
[NDC 2019] Enterprise-Grade Serverless
 
[NDC 2019] Functions 2.0: Enterprise-Grade Serverless
[NDC 2019] Functions 2.0: Enterprise-Grade Serverless[NDC 2019] Functions 2.0: Enterprise-Grade Serverless
[NDC 2019] Functions 2.0: Enterprise-Grade Serverless
 
Securing your Pulsar Cluster with Vault_Chris Kellogg
Securing your Pulsar Cluster with Vault_Chris KelloggSecuring your Pulsar Cluster with Vault_Chris Kellogg
Securing your Pulsar Cluster with Vault_Chris Kellogg
 
Construindo APIs Usando Rails
Construindo APIs Usando RailsConstruindo APIs Usando Rails
Construindo APIs Usando Rails
 
Sharding and Load Balancing in Scala - Twitter's Finagle
Sharding and Load Balancing in Scala - Twitter's FinagleSharding and Load Balancing in Scala - Twitter's Finagle
Sharding and Load Balancing in Scala - Twitter's Finagle
 

Dernier

+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
Health
 
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
masabamasaba
 
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
9953056974 Low Rate Call Girls In Saket, Delhi NCR
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
VishalKumarJha10
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
masabamasaba
 
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
masabamasaba
 

Dernier (20)

+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
 
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
 
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
 
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
 
Generic or specific? Making sensible software design decisions
Generic or specific? Making sensible software design decisionsGeneric or specific? Making sensible software design decisions
Generic or specific? Making sensible software design decisions
 
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
Direct Style Effect Systems -The Print[A] Example- A Comprehension AidDirect Style Effect Systems -The Print[A] Example- A Comprehension Aid
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
 
Define the academic and professional writing..pdf
Define the academic and professional writing..pdfDefine the academic and professional writing..pdf
Define the academic and professional writing..pdf
 
VTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learnVTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learn
 
Microsoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdfMicrosoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdf
 
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
 
%in kempton park+277-882-255-28 abortion pills for sale in kempton park
%in kempton park+277-882-255-28 abortion pills for sale in kempton park %in kempton park+277-882-255-28 abortion pills for sale in kempton park
%in kempton park+277-882-255-28 abortion pills for sale in kempton park
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Models
 
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
 
Exploring the Best Video Editing App.pdf
Exploring the Best Video Editing App.pdfExploring the Best Video Editing App.pdf
Exploring the Best Video Editing App.pdf
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
 
10 Trends Likely to Shape Enterprise Technology in 2024
10 Trends Likely to Shape Enterprise Technology in 202410 Trends Likely to Shape Enterprise Technology in 2024
10 Trends Likely to Shape Enterprise Technology in 2024
 
Announcing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK SoftwareAnnouncing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK Software
 
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
 
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
%+27788225528 love spells in Colorado Springs Psychic Readings, Attraction sp...
 

Finatra v2

  • 1. Finatra v2 Fast, testable Scala services Steve Cosenza @scosenza Finatra & Data API Tech Lead @twitter
  • 2. What is Finatra? • Finatra is a framework for easily building API services on top of Twitter’s Scala stack (twitter-server, finagle, twitter-util) • Currently supports building HTTP services • Plan to support building Thrift services
  • 3. Project History • Feb 2012: Finatra project started on Github by two Twitter engineers (Julio Capote and Chris Burnett) • June 2014: Christopher Coco and Steve Cosenza on the Data API team release Finatra v2.0.0-SNAPSHOT internally at Twitter • Jan 2015: Finatra v1.6 released for Scala 2.9 and 2.10 • April 2015: Finatra v2.0.0M1 publicly released for Scala 2.10 and 2.11
  • 4. Highlights • Production use as Twitter’s HTTP framework • ~50 times faster than v1.6 in several benchmarks • Powerful feature and integration test support • JSR-330 Dependency Injection using Google Guice • Jackson based JSON parsing supporting required fields, default values, and custom validations • Logback MDC integration for contextual logging across Futures • Guice request scope integration with Futures
  • 5. Twitter Inject Libraries • inject-core • inject-app • inject-server • inject-modules • inject-thrift-client • inject-request-scope
  • 6. Finatra Libraries • finatra-http • finatra-jackson • finatra-logback • finatra-httpclient • finatra-utils
  • 7. Tutorial Let’s create a simple HTTP API for Tweets :-)
  • 8. Create a FeatureTest class TwitterCloneFeatureTest extends Test { 
 val server = new EmbeddedHttpServer(
 twitterServer = new HttpServer {})
 
 "Post tweet" in {
 server.httpPost(
 path = "/tweet",
 postBody = """
 {
 "message": "Hello #SFScala",
 "location": {
 "lat": "37.7821120598956",
 "long": "-122.400612831116"
 },
 "sensitive": false
 }”"", andExpect = Created, withLocationHeader = "/tweet/123")
 }
 }
  • 9. And let’s assert the response body "Post tweet" in {
 server.httpPost(
 path = "/tweet",
 postBody = """
 {
 "message": "Hello #SFScala",
 "location": {...} "sensitive": false
 }”"",
 andExpect = Created,
 withLocationHeader = "/tweet/123",
 withJsonBody = """
 {
 "id": "123",
 "message": "Hello #SFScala",
 "location": {
 "lat": "37.7821120598956",
 "long": "-122.400612831116"
 },
 "sensitive": false
 }""")
 }
  • 10. Test Failure: Route not found =========================================================================== HTTP POST /tweet [Header] Content-Length -> 204 [Header] Content-Type -> application/json;charset=utf-8 [Header] Host -> 127.0.0.1:60102 { "message" : "Hello #SFScala", "location" : { "lat" : "37.7821120598956", "long" : "-122.400612831116" }, "sensitive" : false } =========================================================================== RoutingService Request("POST /tweet", from /127.0.0.1:60103) not found in registered routes: --------------------------------------------------------------------------- [Status] 404 Not Found [Header] Content-Length -> 0 *EmptyBody*
  • 11. Create a Controller package finatra.quickstart.controllers
 
 import com.twitter.finagle.http.Request
 import com.twitter.finatra.http.Controller
 
 class TweetsController extends Controller {
 
 post("/tweet") { request: Request =>
 response.created("hello")
 }
 }
  • 12. Create a Server import com.twitter.finatra.HttpServer
 import com.twitter.finatra.filters.CommonFilters
 import com.twitter.finatra.routing.HttpRouter
 import finatra.quickstart.controllers.TweetsControllerV1
 
 class TwitterCloneServer extends HttpServer {
 
 override def configureHttp(router: HttpRouter): Unit = {
 router
 .filter[CommonFilters]
 .add(new TweetsController)
 }
 }
  • 13. Add Server to FeatureTest class TwitterCloneFeatureTest extends Test { 
 val server = new EmbeddedHttpServer(
 twitterServer = new TwitterCloneServer)
 
 ... }
  • 14. Test Failure: Unexpected response body =========================================================================== HTTP POST /tweet [Header] Content-Length -> 204 [Header] Content-Type -> application/json;charset=utf-8 [Header] Host -> 127.0.0.1:60118 ... =========================================================================== 127.0.0.1 - - [24/Apr/2015:21:36:25 +0000] "POST /tweet HTTP/1.1" 201 5 9 "-" --------------------------------------------------------------------------- [Status] 201 Created [Header] Content-Length -> 5 hello Unrecognized token 'hello': was expecting ('true', 'false' or 'null') at [Source: hello; line: 1, column: 11] JSON DIFF FAILED! * Received: hello Expected: {“id":"123","location":{"lat":"37.7821120598956","long":"-122.4006 ...
  • 15. Let’s parse some JSON {
 "message": "Hello #SFScala",
 "location": {
 "lat": "37.7821120598956",
 "long": "-122.400612831116"
 },
 "sensitive": false
 } case class PostedTweet(
 message: String,
 location: Option[Location],
 sensitive: Boolean = false) {
 
 def toStatus(id: StatusId) = { ... }
 } case class Location(
 lat: Double,
 long: Double)

  • 16. Update Controller to use PostedTweet class TweetsController extends Controller {
 
 post("/tweet") { postedTweet: PostedTweet =>
 response.created(postedTweet)
 }
 }

  • 17. Test failure: ’id’ field in body missing [Status] 201 Created [Header] Content-Type -> application/json; charset=utf-8 [Header] Content-Length -> 107 { "message" : "Hello #SFScala", "location" : { "lat" : 37.7821120598956, "long" : -122.400612831116 }, "sensitive" : false } JSON DIFF FAILED! * Received: {“location”:{"lat":37.7821120598956,"long":-122.400612831116}, ... Expected: {“id”:”123","location":{"lat":"37.7821120598956","long":"-122. ...
  • 18. Let’s generate some JSON {
 "id": 123,
 "message": "Hello #SFScala",
 "location": {
 "lat": "37.7821120598956",
 "long": "-122.400612831116"
 },
 "sensitive": false
 } case class RenderableTweet(
 id: StatusId,
 message: String,
 location: Option[RenderableLocation],
 sensitive: Boolean)
 case class StatusId(
 id: String)
 extends WrappedValue[String] case class RenderableLocation(
 lat: String,
 long: String)
  • 19. Update Controller to use RenderableTweet class TweetsController extends Controller {
 
 post("/tweet") { postedTweet: PostedTweet =>
 val statusId = StatusId("123")
 val status = postedTweet.toDomain(statusId) // Save status here 
 val renderableTweet = RenderableTweet.fromDomain(status)
 response
 .created(renderableTweet)
 .location(renderableTweet.id)
 }
 }

  • 20. Feature Test Success! =========================================================================== HTTP POST /tweet ... =========================================================================== 127.0.0.1 - - [24/Apr/2015:22:38:31 +0000] "POST /tweet HTTP/1.1" 201 122 642 "-" --------------------------------------------------------------------------- [Status] 201 Created [Header] Content-Type -> application/json; charset=utf-8 [Header] Location -> http://127.0.0.1:60352/tweet/123 [Header] Content-Length -> 122 { "id" : "123", "message" : "Hello #SFScala", "location" : { "lat" : "37.7821120598956", "long" : "-122.400612831116" }, "sensitive" : false }
  • 21. Let’s test that ‘message’ is required "Missing message field" in {
 server.httpPost(
 path = "/tweet",
 postBody = """
 {
 "location": {
 "lat": "37.7821120598956",
 "long": "-122.400612831116"
 },
 "sensitive": false
 }""",
 andExpect = BadRequest)
 }
  • 22. Test Success =========================================================================== 127.0.0.1 - - [24/Apr/2015:22:57:56 +0000] "POST /tweet HTTP/1.1" 400 42 570 "-" --------------------------------------------------------------------------- [Status] 400 Bad Request [Header] Content-Type -> application/json;charset=utf-8 [Header] Content-Length -> 42 { "errors" : [ "message is a required field" ] }
  • 23. Let’s test for invalid values "Invalid fields" in {
 server.httpPost(
 path = "/tweet",
 postBody = """
 {
 "message": "",
 "location": {
 "lat": "9999",
 "long": "-122.400612831116"
 },
 "sensitive": false
 }""",
 andExpect = BadRequest)
 }
  • 24. Test Failed (expected 400 Bad Request) =========================================================================== 127.0.0.1 - - [25/Apr/2015:01:58:27 +0000] "POST /tweet HTTP/1.1" 201 98 742 "-" --------------------------------------------------------------------------- [Status] 201 Created [Header] Content-Type -> application/json; charset=utf-8 [Header] Location -> http://127.0.0.1:49189/tweet/123 [Header] Content-Length -> 98 { "id" : "123", "message" : "", "location" : { "lat" : "9999.0", "long" : "-122.400612831116" }, "sensitive" : false } 201 Created did not equal 400 Bad Request
  • 25. Validation Annotations • CountryCode • FutureTime • PastTime • Max • Min • NonEmpty • OneOf • Range • Size • TimeGranularity • UUID • MethodValidation
  • 26. Let’s add some validation annotations case class PostedTweet(
 @Size(min = 1, max = 140) message: String,
 location: Option[Location],
 sensitive: Boolean = false) { case class Location(
 @Range(min = -90, max = 90) lat: Double,
 @Range(min = -180, max = 180) long: Double)

  • 27. Test Success =========================================================================== 127.0.0.1 - - [24/Apr/2015:23:01:10 +0000] "POST /tweet HTTP/1.1" 400 106 660 "-" --------------------------------------------------------------------------- [Status] 400 Bad Request [Header] Content-Type -> application/json;charset=utf-8 [Header] Content-Length -> 106 { "errors" : [ "message size [0] is not between 1 and 140", "location.lat [9999.0] is not between -90 and 90" ] }
  • 28. Next let’s write a class to save a tweet @Singleton
 class TweetsService @Inject()(
 idService: IdService,
 firebase: FirebaseClient) {
 
 def save(postedTweet: PostedTweet): Future[Status] = {
 for {
 id <- idService.getId()
 status = postedTweet.toStatus(id) path = s"/statuses/${status.id}.json"
 _ <- firebase.put(path, status)
 } yield status
 }
 }
  • 29. Inject TweetsService into Controller @Singleton
 class TweetsController @Inject()(
 tweetsService: TweetsService)
 extends Controller {
 
 post("/tweet") { postedTweet: PostedTweet =>
 tweetsService.save(postedTweet) map { status => val renderableTweet = RenderableTweet.fromDomain(status)
 response
 .created(renderableTweet)
 .location(renderableTweet.id)
 }
 }
 }
  • 30. Update Server //Before class TwitterCloneServer extends HttpServer {
 
 override def configureHttp(router: HttpRouter): Unit = {
 router
 .filter[CommonFilters]
 .add(new TweetsController)
 }
 } //After class TwitterCloneServer extends HttpServer {
 
 override def modules = Seq(FirebaseHttpClientModule)
 
 override def configureHttp(router: HttpRouter): Unit = {
 router
 .filter[CommonFilters]
 .add[TweetsController]
 }
 }
  • 31. FirebaseHttpClientModule object FirebaseHttpClientModule extends HttpClientModule {
 
 override val dest = "flag!firebase"
 
 override def retryPolicy = Some(exponentialRetry(
 start = 10.millis,
 multiplier = 2,
 numRetries = 3,
 shouldRetry = Http4xxOr5xxResponses))
 }

  • 32. Rerun ‘Post tweet’ Test "Post tweet" in {
 server.httpPost(
 path = "/tweet",
 postBody = ...,
 andExpect = Created,
 withLocationHeader = "/tweet/123",
 withJsonBody = """
 {
 "id": "123",
 "message": "Hello #SFScala",
 "location": {
 "lat": "37.7821120598956",
 "long": "-122.400612831116"
 },
 "sensitive": false
 }""")
 }
  • 33. Test Fails: “no hosts are available” =========================================================================== HTTP POST /tweet { "message" : "Hello #SFScala", ... } =========================================================================== Request("PUT /statuses/374ea2dd-846a-45a5-a296-bcf43e5830c3.json”) service unavailable com.twitter.finagle.NoBrokersAvailableException: No hosts are available for flag!firebase 127.0.0.1 - - [25/Apr/2015:02:37:01 +0000] "POST /tweet HTTP/1.1" 503 34 584 "-" --------------------------------------------------------------------------- [Status] 503 Service Unavailable [Header] Content-Type -> application/json;charset=utf-8 [Header] Content-Length -> 34 { "errors" : [ "service unavailable" ] }
  • 34. Let’s mock our services // Before
 class TwitterCloneFeatureTest extends Test {
 val server = new EmbeddedHttpServer(
 twitterServer = new TwitterCloneServer)
 
 "Post tweet" in { ... } } // After
 class TwitterCloneFeatureTest extends FeatureTest with Mockito {
 
 override val server = new EmbeddedHttpServer(
 twitterServer = new TwitterCloneServer {
 override val overrideModules = Seq(integrationTestModule)
 })
 
 @Bind val firebaseClient = smartMock[FirebaseClient]
 
 @Bind val idService = smartMock[IdService]
 
 “Post tweet" in { ... } }
  • 35. Let’s mock our services class TwitterCloneFeatureTest extends FeatureTest with Mockito {
 
 override val server = new EmbeddedHttpServer(
 twitterServer = new TwitterCloneServer {
 override val overrideModules = Seq(integrationTestModule)
 })
 
 @Bind val firebaseClient = smartMock[FirebaseClient]
 
 @Bind val idService = smartMock[IdService]
 
 “Post tweet" in { val statusId = StatusId("123")
 idService.getId returns Future(statusId)
 
 val putStatus = Status(id = statusId, ...)
 firebaseClient.put("/statuses/123.json", putStatus) returns Future.Unit val result = server.httpPost(
 path = "/tweet", ...)

  • 36. Feature Test Success! =========================================================================== HTTP POST /tweet ... =========================================================================== 127.0.0.1 - - [24/Apr/2015:22:38:31 +0000] "POST /tweet HTTP/1.1" 201 122 642 "-" --------------------------------------------------------------------------- [Status] 201 Created [Header] Content-Type -> application/json; charset=utf-8 [Header] Location -> http://127.0.0.1:60352/tweet/123 [Header] Content-Length -> 122 { "id" : "123", "message" : "Hello #SFScala", "location" : { "lat" : "37.7821120598956", "long" : "-122.400612831116" }, "sensitive" : false }
  • 37. Startup Test class TwitterCloneStartupTest extends Test {
 
 val server = new EmbeddedHttpServer(
 stage = PRODUCTION,
 twitterServer = new TwitterCloneServer,
 clientFlags = Map(
 "com.twitter.server.resolverMap" -> "firebase=nil!"))
 
 "server" in {
 server.assertHealthy()
 }
 }
  • 38. Guice with Startup Tests • Guice provides significant modularity and testing benefits • Startup tests mostly mitigate lack of compile time safety • We’ve found the combination to be a good compromise
  • 39. Additional v2 Features • Message body readers and writers • Declarative request parsing • Injectable flags • Mustache Templates • Multi-Server Tests • Server Warmup
  • 40. Message body readers and writers class StatusMessageBodyWriter extends MessageBodyWriter[Status] {
 
 override def write(status: Status): WriterResponse = {
 WriterResponse(
 MediaType.JSON_UTF_8,
 RenderableTweet.fromDomain(status))
 }
 }
  • 41. Declarative request parsing case class GetTweetRequest(
 @RouteParam id: StatusId,
 @QueryParam expand: Boolean = false) case class GetTweetsRequest(
 @Range(min = 1, max = 100) @QueryParam count: Int = 50) @Singleton
 class TweetsController extends Controller { 
 get(“/tweet/:id”) { request: GetTweetRequest =>
 ... } get(“/tweets/?”) { request: GetTweetsRequest =>
 ... }
 }
  • 42. Injectable flags @Singleton
 class TweetsService @Inject()(
 @Flag("max.count") maxCount: Int,
 idService: IdService,
 firebase: FirebaseClient) { ... }
  • 43. Mustache templates // user.mustache id:{{id}} name:{{name}}
 @Mustache("user")
 case class UserView(
 id: Int,
 name: String)
 get("/") { request: Request =>
 UserView(
 123,
 "Bob")
 }
  • 44. Server Warmup class TwitterCloneServer extends HttpServer {
 override val resolveFinagleClientsOnStartup = true
 
 override def warmup() {
 run[TwitterCloneWarmup]()
 } ...
 }
  • 45. Multi-Server tests class EchoHttpServerFeatureTest extends Test {
 
 val thriftServer = new EmbeddedThriftServer(
 twitterServer = new EchoThriftServer)
 
 val httpServer = new EmbeddedHttpServer(
 twitterServer = new EchoHttpServer,
 clientFlags = Map(
 resolverMap(“flag!echo-service“ -> thriftServer.thriftExternalPort)))
 
 "EchoHttpServer" should {
 "Echo msg" in {
 httpServer.httpGet(
 path = "/echo?msg=Bob",
 andExpect = Ok,
 withBody = "Bob")
 }
 }
 }

  • 46. Finatra Contributors • Steve Cosenza • Christopher Coco • Jason Carey • Eugene Ma
  • 47. Questions? class TwitterCloneServer extends HttpServer {
 override val resolveFinagleClientsOnStartup = true
 override val modules = Seq(FirebaseHttpClientModule)
 override def warmup { run[TwitterCloneWarmup]() } override def configureHttp(router: HttpRouter): Unit = {
 router
 .register[StatusMessageBodyWriter]
 .filter[CommonFilters]
 .add[TweetsController]
 }
 } @Singleton
 class TweetsController @Inject()(
 tweetsService: TweetsService)
 extends Controller {
 post("/tweet") { postedTweet: PostedTweet =>
 tweetsService.save(postedTweet) map { status =>
 response
 .created(status)
 .location(status.id)
 }
 }
 }