Voyager avec play scala

1,287 views

Published on

Retour d'expérience sur le développement d'une application avec Play2/Scala/Redis pendant un an chez Zaptravel.

Published in: Technology

Voyager avec play scala

  1. 1. Voyager avec Play 2 Nicolas Martignole nicolas@touilleur-express.fr @nmartignole Scala.IO - 24/25 octobre 2013, Paris samedi 26 octobre 13
  2. 2. Votre plan de vol What Why How ZapTravel samedi 26 octobre 13
  3. 3. Avant de commencer... Current status samedi 26 octobre 13
  4. 4. ZapTravel samedi 26 octobre 13
  5. 5. We search destinations & dates, then find the best price, hotel and transport, so you don’t need to samedi 26 octobre 13
  6. 6. We search destinations & dates, then find the best price, hotel and transport, so you don’t need to filter, map, reduce samedi 26 octobre 13
  7. 7. ZapTravel samedi 26 octobre 13
  8. 8. samedi 26 octobre 13
  9. 9. samedi 26 octobre 13
  10. 10. samedi 26 octobre 13
  11. 11. Mobile API REST samedi 26 octobre 13
  12. 12. samedi 26 octobre 13
  13. 13. samedi 26 octobre 13
  14. 14. samedi 26 octobre 13
  15. 15. samedi 26 octobre 13
  16. 16. Show me Romance deals samedi 26 octobre 13
  17. 17. Show me Family deals samedi 26 octobre 13
  18. 18. samedi 26 octobre 13
  19. 19. </zaptravel> samedi 26 octobre 13
  20. 20. “ There are known knowns; there are things we know that we know. There are known unknowns; that is to say, there are things that we now know we don't know. But there are also unknown unknowns – there are things we do not know we don't know. ” —United States Secretary of Defense, Donald Rumsfeld samedi 26 octobre 13
  21. 21. Aware Don’t know Know Not aware samedi 26 octobre 13
  22. 22. Aware Don’t know Know Not aware samedi 26 octobre 13
  23. 23. Quelques chiffres • 159 000 hôtels • 1383 destinations • 840 000 transports (avions/trains) • 1.4To images sur S3 • 20600 prix chambres hôtels samedi 26 octobre 13
  24. 24. redis 127.0.0.1:6379> hlen Hotel:Content:Short (integer) 158 041 samedi 26 octobre 13
  25. 25. Ce que je savais samedi 26 octobre 13
  26. 26. Ce que je savais • Play! Framework samedi 26 octobre 13
  27. 27. Ce que je savais • Play! Framework • Web development samedi 26 octobre 13
  28. 28. Ce que je savais • Play! Framework • Web development • Hiring and training developers samedi 26 octobre 13
  29. 29. Ce que je savais • Play! Framework • Web development • Hiring and training developers • Kiss-ass project managment samedi 26 octobre 13
  30. 30. Equipe Ze Boss Java HTML CSS Scala Play2 Scala samedi 26 octobre 13
  31. 31. Equipe Ze Boss Java HTML CSS Scala Play2 Scala 5 3,75 2,5 1,25 0 samedi 26 octobre 13 Mai 2012 Ete 2012 Sept 2012 Oct 2012 Nov 2012 Jan 2013 Oct 2013
  32. 32. samedi 26 octobre 13
  33. 33. Comment apprendre Scala (et désapprendre Java) samedi 26 octobre 13
  34. 34. Scala et Zaptravel • Scala => recrutement • Facile à apprendre • Scala c’est simple samedi 26 octobre 13
  35. 35. Ah tu fais du Scala samedi 26 octobre 13
  36. 36. Paradigme objet ET fonctionnel http://parleys.com/p/51c1994ae4b0d38b54f4621b samedi 26 octobre 13
  37. 37. Ce que j’ai évité - 18 samedi 26 octobre 13
  38. 38. SBT samedi 26 octobre 13
  39. 39. SBT ScalaZ samedi 26 octobre 13
  40. 40. Les choses que je ne savais pas • Faut être gonflé • Communauté • Parallélisme, Reactivité • Play2/Scala/Redis en PROD ??? • SEO samedi 26 octobre 13 • JSON+Redis • Typesafe / refactoring
  41. 41. samedi 26 octobre 13
  42. 42. Communauté Scala Place de Scala, la communauté, par rapport à Java samedi 26 octobre 13
  43. 43. Scala Days 2013 on parleys.com 94 516 VUEs samedi 26 octobre 13
  44. 44. Parallélisme et concurrence It’s the web, stupid Response[HTML] = Fx(Request) samedi 26 octobre 13
  45. 45. Typesafe •HTML template •routes •config •LESS samedi 26 octobre 13
  46. 46. Play2 -> Reactive Reactive In Practice samedi 26 octobre 13
  47. 47. Play2 -> Reactive Reactive In Practice samedi 26 octobre 13
  48. 48. Iteratee, Enumeratee and Enumerator on Zaptravel août - sept 2012 R.I.P samedi 26 octobre 13
  49. 49. BusinessException Iteratee/Enumeratee/Enumerator c’est cool, mais nous n’en n’avons pas besoin pour le moment. samedi 26 octobre 13
  50. 50. /** * Server sent event streaming controller. * Date: 06/08/12 * Time: 12:16 */ object Streaming extends Controller {   // Streaming using server sent event   def stream(requestId: String) = Action {     // Define an implicit EventNameExtractor wich extract the "event" name from the Json event so that the EventSource() sets the event in the message     implicit val eventNameExtractor: EventNameExtractor[JsValue]=EventNameExtractor[JsValue](eventName = (zepEvent)=>zepEvent. ("event").asOpt[String])         // Streams.events is a composition of HotelPrice and AirfarePrice.         Ok.feed(Streams.events(requestId) &> EventSource()).as("text/event-stream")   }  implicit val eventNameExtractor: EventNameExtractor[JsValue] = EventNameExtractor[JsValue](eventName = (zepEvent)=>zepEvent. ("event").asOpt[String]) samedi 26 octobre 13
  51. 51. Akka faukon samedi 26 octobre 13
  52. 52. Akka • Cron Jobs • Emails (Mailjet/Mailchimp) • Facebook • ElasticSearch index (proto) • Sitemap • Generate content (R.I.P.) samedi 26 octobre 13
  53. 53. Akka samedi 26 octobre 13
  54. 54. Dev ? Redis Prices read-only Play2 Prix Hotels, Avions, Voitures, Trains, Rating Hotel Redis read/write Static Lieux, Destinations, Contenu, Routage, URLs, Places, Tags, Webuser Redis slave-of EC2 m2.xlarge S3 samedi 26 octobre 13 1.2 GB 450k obj EC2 m2.2xlarge 27GB 870k obj
  55. 55. Prod ? CloudWatch www Route53 Cloudfront redis prices EC2 m2.2xlarge ELB redis static EC2 c1.medium S3 EC2 m2.xlarge logs SimpleDB samedi 26 octobre 13 redis backup
  56. 56. Play2 + AWS samedi 26 octobre 13
  57. 57. Idéal : c1.medium 2 vCPUs 1.7 GB mémoire samedi 26 octobre 13
  58. 58. Redis samedi 26 octobre 13
  59. 59. Ce qu’il faut retenir 2300 USD / mois • IaaS versus PaaS pour Zaptravel samedi 26 octobre 13
  60. 60. Boarding... samedi 26 octobre 13
  61. 61. Des cas d’usages samedi 26 octobre 13
  62. 62. Fonctionnalités samedi 26 octobre 13
  63. 63. Fonctionnalités • API REST samedi 26 octobre 13
  64. 64. Fonctionnalités • API REST • Facebook samedi 26 octobre 13
  65. 65. Fonctionnalités • API REST • Facebook • Weather samedi 26 octobre 13
  66. 66. Fonctionnalités • API REST • Facebook • Weather • GeoIP samedi 26 octobre 13
  67. 67. Fonctionnalités • API REST • Facebook • Weather • GeoIP • Semantic Search samedi 26 octobre 13
  68. 68. Fonctionnalités • API REST • Facebook • Weather • GeoIP • Semantic Search • Cache Redis samedi 26 octobre 13
  69. 69. Fonctionnalités • API REST • Facebook • Weather • GeoIP • Semantic Search • Cache Redis samedi 26 octobre 13 • Mobile Web version
  70. 70. Fonctionnalités • API REST • Facebook • Weather • GeoIP • Semantic Search • Cache Redis samedi 26 octobre 13 • Mobile Web version • Authentification
  71. 71. Fonctionnalités • API REST • Facebook • Weather • GeoIP • Semantic Search • Cache Redis samedi 26 octobre 13 • Mobile Web version • Authentification • Statistiques/Parcours visiteur
  72. 72. Fonctionnalités • API REST • Facebook • Weather • GeoIP • Semantic Search • Cache Redis samedi 26 octobre 13 • Mobile Web version • Authentification • Statistiques/Parcours visiteur
  73. 73. Quelques exemples ZapTravel samedi 26 octobre 13
  74. 74. Charger une donnée venant de Redis ZapTravel samedi 26 octobre 13
  75. 75. Architecture Redis Air/Hotel/Cars/Ac Web HTTP HTTPS Web LB Redis Resa/Users Redis Web Content Web ZapTravel samedi 26 octobre 13
  76. 76. Architecture Redis Web Air/Hotel/Cars/Ac Web HTTP HTTPS Web LB Redis Resa/Users Redis Web Content redis Web ZapTravel samedi 26 octobre 13
  77. 77. Cas d’usage Donne moi le label qui correspond à originId =380 ZapTravel samedi 26 octobre 13
  78. 78. Cas d’usage Donne moi le label qui correspond à originId =380 def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)) } ZapTravel samedi 26 octobre 13
  79. 79. Cas d’usage Donne moi le label qui correspond à originId =380 def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)) } ZapTravel samedi 26 octobre 13
  80. 80. Cas d’usage Donne moi le label qui correspond à originId =380 def getSlug(originId: Long): Option[String] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)) } Driver Sedis https://github.com/pk11/sedis samedi 26 octobre 13 ZapTravel
  81. 81. Un mot sur les Tests samedi 26 octobre 13
  82. 82. package models   import org.specs2.mutable._   import play.api.test._ import play.api.test.Helpers._   class OriginSpecs extends Specification { "An Origin" should { "returns the slug for a valid origin" in { running(FakeApplication()) { Origin.getSlug(380) mustEqual Some("from-london") Origin.getSlug(1) mustEqual Some("from-paris") Origin.getSlug(-9999) mustEqual None } } } } https://gist.github.com/nicmarti/5064048 samedi 26 octobre 13
  83. 83. Charger un objet Charge moi un Objet «Londres» ZapTravel samedi 26 octobre 13
  84. 84. Charger un objet Origin 1) charger from-london def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)).map{ slug=> Option(client.hget("Places:Place:"+originId, "display").map { .... ... } } } ZapTravel samedi 26 octobre 13
  85. 85. Code smells 2) charger display... def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => Option(client.hget("Url:From:Rev", originId.toString)).map{ slug=> Option(client.hget("Places:Place:"+originId, "display").map{ .... ... } } } ZapTravel samedi 26 octobre 13
  86. 86. Cas d’usage 2) charger display... def getOrigin(originId: Long): Option[Origin] = Redis.pool.withClient { client => for(slug<-Option(client.hget("Url:From:Rev", originId.toString)); display<-Option(client.hget("Places:Place:"+originId,"display") )) yield Origin(originId,display,slug) } } for-comprehension https://gist.github.com/nicmarti/5064066 ZapTravel samedi 26 octobre 13
  87. 87. La Tour Eiffel 1. Charger du JSON à partir de Redis 2. Interpréter et retourner un objet PointOfInterest ZapTravel samedi 26 octobre 13
  88. 88. HGET Pois:PoisHash 52511 {"name":"Eiffel Tower","address":"","latitude":"48.8582493546","longitude":"2.2945117950","website":"www.toureiffel.fr","rank":3,"photo":{"r":"eiffel-tower-paris-france","k":"6b56","e":"jpg","w":2406,"h": 1600,"a":"Mirari Erdoiza","l":"http://www.fotopedia.com/items/anboto-RiKxAA3gE6I"},"sentences": {"gbs":[{"d":"The Eiffel Tower is one of the most famous monuments in the world (324 metres, 10,100 tonnes).","a":"Paris","l":"http://www.paris.com/paris_landmarks/monuments/ eiffel_tower_paris"},{"d":"This is without doubt one of the most recognizable structures in the world.","a":"Frommers","l":"http://www.frommers.com/destinations/paris/A25288.html"},{"d":"If the Statue of Liberty is emblematic of New York, Big Ben is London, and the Kremlin is Moscow, then the Eiffel Tower is the symbol of Paris.","a":"Fodors","l":"http://www.fodors.com/world/europe/ france/paris/review-97417.html"},{"d":"When it was built for the 1889 Exposition Universelle (World Fair), marking the centenary of the Revolution, the Tour Eiffel faced massive opposition from Paris' artistic and literary elite.","a":"Lonely Planet","l":"http://www.lonelyplanet.com/france/paris /sights/famous-landmark/eiffel-tower"}],"tips":[{"d":"It's pretty high!.","a":"annawelford","l":"http://www.lonelyplanet.com/france/paris/sights/famouslandmark/eiffel-tower","s":"Lonely Planet"},{"d":"Bigger than you think.","a":"anomolly","l":"http:/ /www.lonelyplanet.com/france/paris/sights/famous-landmark/eiffel-tower","s":"Lonely Planet"},{"d":"Overcrowded.","a":"anshjain","l":"http://www.lonelyplanet.com/france/paris/ sights/famous-landmark/eiffel-tower","s":"Lonely Planet"},{"d":"The restaurant on the first floor is an amazing experience!.","a":"ansofie","l":"http://www.lonelyplanet.com/france/paris/sights /famous-landmark/eiffel-tower","s":"Lonely Planet"}]},"tags":["Landmark","Memorials/ Monuments","Sights","Famous landmark"]} samedi 26 octobre 13
  89. 89. Play 2.1 • Définir une case class POI • Définir un implicit Json.format[POI] • C’est tout... ou presque samedi 26 octobre 13
  90. 90. Play 2.0 case class POI(name: String, address: String, latitude: String, longitude: String, website: Option[String], photo: Option[SightPhoto] = None, sentences: Sentences, tags: Option[List[String]]) POI = Point of Interest = notre Tour Eiffel samedi 26 octobre 13
  91. 91. Play 2.0 samedi 26 octobre 13
  92. 92. Play 2.1 samedi 26 octobre 13
  93. 93. Play 2.1 (Parser lorsque le JSON stocké sur Redis utilise une déclaration différente de la case class) samedi 26 octobre 13
  94. 94. Appel Redis et interprétation JSON samedi 26 octobre 13
  95. 95. Afficher une liste ZapTravel samedi 26 octobre 13
  96. 96. Afficher une liste ZapTravel samedi 26 octobre 13
  97. 97. Aller sur Redis def allOrigins: List[Origin] = Redis.pool.withClient { client => // ... // ... } Modèle samedi 26 octobre 13
  98. 98. Préparer une liste def allUrlOrigins: Seq[(String, String)] = { Origin.allOrigins.map{ origin => (origin.slug, origin.label) }.sortBy(_._2) } Contrôleur samedi 26 octobre 13
  99. 99. Envoyer la liste au template Code dans la page HTML <label for="location">Your travel origin is :</label> @select( userForm("originCity"), FolioCriteria.allUrlOrigins , '_label -> "Travel from origin", '_showConstraints -> false ) Vue samedi 26 octobre 13
  100. 100. Afficher une liste ZapTravel samedi 26 octobre 13
  101. 101. Gérer l’authentification samedi 26 octobre 13
  102. 102. Comment protéger l’accès à une ressource ? My Info samedi 26 octobre 13
  103. 103. Comment protéger l’accès à une ressource ? My Info samedi 26 octobre 13
  104. 104. Dans le Controller object Application extends Controller { def index = Action { implicit request => val username="test" Ok(html.index(username)) } } samedi 26 octobre 13
  105. 105. Dans le Controller object Application extends Controller with Secured { def index = ActionSecure { username => implicit request => Ok(html.index(username)) } } samedi 26 octobre 13
  106. 106. trait Secured { def username(request: RequestHeader) = request.session.get(Security.username) def onUnauthorized(request: RequestHeader) = Results.Redirect(routes.Auth.login) def ActionSecure(f: => String => Request[AnyContent] => Result) = { Security.Authenticated(username, onUnauthorized) { user => Action{ request => f(user)(request) } } } Result } String samedi 26 octobre 13 Request[AnyContent] HTML
  107. 107. Play2 et Sécurité • Simple • Composable • Facile à tester samedi 26 octobre 13
  108. 108. Optimiser l’indexation et le référencement samedi 26 octobre 13
  109. 109. Indexation et référencement • URLs propres et pondérées • Mots clés • Liens et Sitemap • Microformat (Hotel, Avion, Lieux) • Contenu non répété samedi 26 octobre 13
  110. 110. samedi 26 octobre 13
  111. 111. routes Compilé et validé samedi 26 octobre 13
  112. 112. URL /from-boston/quality GET /$origin<from-(.*)>/:classifier controllers.Frontoffice.home(origin:String, classifier: String) http://www.zaptravel.com/romance/weekend-deals/from-paris/to-athens/ 12-Apr-2013-to-14-Apr-2013/elite-athens-greece samedi 26 octobre 13
  113. 113. Play2 • La séparation entre la partie routage et la partie contrôleur permet de créer des URLs «propres» samedi 26 octobre 13
  114. 114. Sitemap • Déclarer la table des matières de son site • Optimise le référencement • Permet de mettre en cache les pages curl http://www.zaptravel.com/sitemap.xml samedi 26 octobre 13
  115. 115. samedi 26 octobre 13
  116. 116. Problème : construire le sitemap de façon asynchrone samedi 26 octobre 13
  117. 117. Solution : Async Akka / Play2 samedi 26 octobre 13
  118. 118. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } } samedi 26 octobre 13
  119. 119. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ... // some other code val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards) ).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } } samedi 26 octobre 13
  120. 120. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } } samedi 26 octobre 13
  121. 121. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) val updatedUrl = (for (l <- localAddress; p <publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } } samedi 26 octobre 13
  122. 122. def sitemap = Action { implicit request => val longCall = Akka.future { val today = ZapFormats.formatForJson(new DateTime()) val listOfSEOCards = FolioCard.allForSEO().flatMap { seo => val localURL = routes.Frontoffice.showFolioDynamically(seo.category, seo.duration, seo.origin, seo.destination, seo.dateRange, seo.segment).absoluteURL(secure = false) Bref... val updatedUrl = (for (l <- localAddress; p <publicAddress) yield localURL.replaceAll(l, p)) updatedUrl } Ok(views.xml.Application.sitemap(today, listOfSEOCards)).as("text/xml") } Async { implicit val timeout = akka.util.Timeout(60 seconds) longCall } } curl http://www.zaptravel.com/sitemap.xml samedi 26 octobre 13
  123. 123. Gestion du cache samedi 26 octobre 13
  124. 124. Comment améliorer les performances ? samedi 26 octobre 13
  125. 125. Eviter de recharger la même page, utilisez code 304 NotModified Note: @rosstuck a fait une session sur HTTP à Confoo mercredi dernier samedi 26 octobre 13
  126. 126. Exemple sur /from-paris/quality Navigateur Play2 GET /from-paris/quality samedi 26 octobre 13
  127. 127. Exemple sur /from-paris/quality Navigateur Play2 OK HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 ETag: 11299930771 Cache-Control: max-age=600, s-maxage=600, must-revalidate Content-Length: 103586 ... ... ce n’est pas une erreur samedi 26 octobre 13
  128. 128. Recharge /from-paris/quality GET /from-paris/quality If-None-Match: 112999307771 Navigateur Play2 304 Not Modified Content-Length: 0 samedi 26 octobre 13
  129. 129. Optimisation 1 • Evitez de faire travailler votre serveur pour rien • Déterminez des ETags «métiers» • Attention à la gestion du cache et des serveurs mandataires. samedi 26 octobre 13
  130. 130. Optimisation 2 Faire de la gestion de cache applicative samedi 26 octobre 13
  131. 131. Cache applicatif ? samedi 26 octobre 13
  132. 132. 2 types de cache Cache technique type Varnish - Process à part - Cache HTTP samedi 26 octobre 13 Cache de Play2 ou Redis - Code applicatif - utilise la mémoire de Play2 ou Redis
  133. 133. 2 types de cache Cache technique type Varnish • Facile à installer • Evite de solliciter Play2 • Scalable • Configurable samedi 26 octobre 13
  134. 134. 2 types de cache Cache applicatif Play2/ Redis • Prend en compte le métier • Permet de garder les pages «authentifiées» • Pas aussi performant que la solution Varnish samedi 26 octobre 13
  135. 135. Sur Zaptravel • Page d’accueil optimisé avec Cache de Play2 • Page Folio, section top Deal avec cache Play2 • Page Deal, cache avec Redis samedi 26 octobre 13
  136. 136. Et pour terminer samedi 26 octobre 13
  137. 137. Merci nicolas@touilleur-express.fr @nmartignole samedi 26 octobre 13

×