42
Sept 06 2014

Xitrum @ Scala Matsuri Tokyo 2014

Embed Size (px)

DESCRIPTION

Xitrum as presented at Scala Matsuri Tokyo 2014

Citation preview

Sept 06 2014

Ngoc Dao

Takeharu Oshida

https://github.com/georgeOsdDev

https://github.com/ngocdaothanh

http://mobilus.co.jp/

What is

Xitrum?Xitrum is an async and clustered !Scala web framework and HTTP(S) server !on top of Netty, Akka

Why you should use

Xitrum?• Featureful!• Easy to use!• High performance

Scala, Netty, and Akka are fast!• Scalable

Can scale to a cluster of servers using Akka cluster and/or Hazelcast

Homepage:http://xitrum-framework.github.io/ (there are various demos) Guides (English, Japanese, Russian): http://xitrum-framework.github.io/guide.html (Korean version is in progress) !

Community (Google Group):https://groups.google.com/forum/#!forum/xitrum-framework

Where

Xitrum is used?KONNECT (Messaging Service)!http://mobilus.co.jp/konnect/!!

KONNECT can be used in mobile games, mobiles apps, SNS websites etc.Xitrum is also being used in France, Korea, Russia, Singapore etc.

Xitrum:!WebSocket (SockJS)!

CORS support

2010-2013 Xitrum 1.x-2.x

2014 Xitrum 3.x• Netty 4.x • Swagger • Component • FileMonitor, i18n • CORS • WebJARs • Glokka • Agent7 (autoreload classes on change)

!!

!

http://bit.ly/xitrum13

http://bit.ly/xitrum-changelog

How Xitrum works?

Client

Netty

Action FutureAction ActorAction

Akka

Xitrum

Thread pool to run FutureAction and

ActorAction

I/O thread pool to accept requests and reply

responses

Client

Run directly on Netty I/O thread

Dispatch request

Async

Netty handler

Netty handler

Netty handler

Netty handler

Xitrum

Your program

http://bit.ly/xitrum-handlers

Akka cluster (code)!Hazelcast (data)

Client

Netty

A FA AA

Akka

Xitrum

ClientClient

Netty

A FA AA

Akka

Xitrum

Client

Server N Server N+1

Embed Xitrumobject MyApp { def main(args: Array[String]) { ... // Somewhere in your app xitrum.Server.start() ... } }

1. Collect routes!!

2. Start HTTP/HTTPS servers

import xitrum.Action import xitrum.annotation.GET !

@GET("hello") class MyAction extends Action { def execute() { respondText("Hello") } }

Action example

FutureAction!ActorAction

Annotations: Scala vs JavaScala: @GET("matsuri", "festival") Java: @GET(Array("matsuri", "festival")) !

Scala: case class GET(paths: String*) extends

scala.annotation.StaticAnnotation !

Java:public @interface GET { String[] value(); }

Benefits of using annotations

Routes in .class and .jar in classpath are automatically collected and merged.

A.class

B.class

lib1.jar

lib2.jar

Routes

Problem with annotationsCollecting routes from .class and .jar files is slow.!!

Solutions:!• In development mode, routes in .class and .jar

files that are not in the current working directory are cached to file routes.cache.

• To avoid loading lots of classes, don't collect routes from: java.xxx, javax.xxx, scala.xxx, sun.xxx, com.sun.xxx

Annotations defined by Xitrum:!http://bit.ly/xitrum-annotations!!

Lib to scan classes in classpath to collect routes:!https://github.com/xitrum-framework/sclasner

username

password

login

Hello ! Hello! How are you? Fine

message send

GET /chatGET /

POST /login SockJS /connect

Demo overview

https://github.com/xitrum-framework/matsuri14

• Simple HTTP CRUD with MongoDB POST /admin/user GET /admin/user GET /admin/user/:userId PUT /admin/user/:userId DELETE /admin/user/:userIdPUT/PATCH/DELETE can be emulated viaPOST with _method=PUT/PATCH/DELETE

!• API documentation with Swagger

Demo overview

DB Cluseter

Xitrum3

Cluster

LB (HAProxy, Nginx,

Route53 etc.)

NoSQL

RDB

Other services

Akka Hazelcast

Xitrum2 Akka Hazelcast

Xitrum1 Akka Hazelcast

https://github.com/xitrum-framework/glokka https://github.com/xitrum-framework/xitrum-hazelcast

Getting started with xitrum-new skeletonhttps://github.com/xitrum-framework/xitrum-new

https://github.com/xitrum-framework/xitrum-scalate

@GET("admin") class AdminIndex extends Action { def execute() { // Get all users val users = User.listAll() // Pass users to view template at("users") = users !

// Response respons view with template respondView() } }

ActorActionFutureAction

Action

- import matsuri.demo.action._!- import matsuri.demo.model.User!!div.row#usersTable! table.table.table-striped#messageTable! thead! tr.bg-primary! th.col-xs-2 =t("Name")! th.col-xs-2 =t("Age")! th.col-xs-2 =t("Desc")! th.col-xs-2 =t("Created time")! th.col-xs-2 =t("Updated time")! th.col-xs-2 =t("Last login time")! tbody! - for (user <- at("users").asInstanceOf[List[User]])! tr! th! a(href={url[AdminUserShow](("name", user.name))}) = user.name! th = user.age! th = user.desc! th = user.createdAtAsStr! th = user.updatedAtAsStr! th = user.lastLoginAsStr

• Scalate template with jade (mustache, scaml, or ssp)

• "at" function • i18n with GNU gettext

View

└── src └── scalate └── matsuri └── demo └── action ├── AdminIndex.jade └── DefaultLayout.jade

package matsuri.demo.action !

import xitrum.Action !

trait DefaultLayout extends Action { override def layout = renderViewNoLayout[DefaultLayout]() }

Layout

!!! 5 html head != antiCsrfMeta != xitrumCss ! meta(content="text/html; charset=utf-8" http-equiv="content-type") title ScalaMatsuri2014 Xitrum Demo ! link(rel="shortcut icon" href={publicUrl("favicon.ico")}) link(type="text/css" rel="stylesheet" media="all" href={webJarsUrl("bootstrap/3.2.0/css", "bootstrap.css", "bootstrap.min.css")}) link(type="text/css" rel="stylesheet" media="all" href={publicUrl("app.css")}) ! body .container h1 ! #flash !~ jsRenderFlash() != renderedView ! != jsDefaults script(src={webJarsUrl("bootstrap/3.2.0/js", "bootstrap.js", "bootstrap.min.js")}) script(src={webJarsUrl("underscorejs/1.6.0", "underscore.js", "underscore-min.js")}) != jsForView

Layout

form(role="form" method="post" action={url[AdminUserCreate]}) != antiCsrfInput div.modal-header button.close(type="button" data-dismiss="modal") span(aria-hidden="true") &times; span.sr-only =t("Close") h4.modal-title#myModalLabel =t("Create New User") div.modal-body div.form-group label(for="newUserName") =t("Name") input.form-control#newUserName(name="name" type="text" placeholder={t("Enter Name")} minlength=5 maxlenght=10 required=true) div.form-group label(for="newUserPass") =t("Password") input.form-control#newUserPass(name="password" type="password" placeholder={t("Enter Password")} minlength=8 required=true) ! div.modal-footer button.btn.btn-default(type="button" data-dismiss="modal") = t("Cancel") button.btn.btn-primary(type="submit") = t("Save")

Form• "url" function • jquery-validation • Anti csrf token

@POST("admin/user") class AdminUserCreate extends AdminAction { def execute() { // Get request paramaters val name = param("name") val password = param("password") // Optional parameters val age = paramo[Int]("age") val desc = paramo("desc") ! Required.exception("name", name) Required.exception("password", password) ! User.create(name, password, age, desc) flash(t("Success")) redirectTo[AdminIndex]() }

Get request params

with param(s)

and param(o)

trait AdminFilter { this: Action => ! beforeFilter { if (SVar.isAdmin.isDefined) true else authBasic() } ! private def authBasic(): Boolean = { basicAuth(Config.basicAuth.realm) { (username, password) => if (username == Config.basicAuth.name && password == Config.basicAuth.pass) { SVar.isAdmin.set(true) true } else { false } } }

object SVar { object isAdmin extends SessionVar[Boolean] }

Use before filter to check authentication info in session

@Swagger( Swagger.Summary("Create User"), Swagger.Response(200, "status = 0: success, 1: failed to create user"), Swagger.Response(400, "Invalid request parameter"), Swagger.StringForm("name"), Swagger.StringForm("password"), Swagger.OptIntForm("age"), Swagger.OptStringForm("desc") )

• /xitrum/swagger • /xitrum/swagger-ui • Create test client with Swagger-codegen

https://github.com/wordnik/swagger-codegen

API doc

https://github.com/wordnik/swagger-ui

https://github.com/wordnik/swagger-spec

@GET("login", "") class LoginIndex extends DefaultLayout { def execute() { respondView() } } !@POST("login") class Login extends Action { def execute() { session.clear() val name = param("name") val password = param("password") ! User.authLogin(name, password) match { case Some(user) => SVar.userName.set(user.name) redirectTo[ChatIndex]() ! case None => flash(t(s"Invalid username or password")) redirectTo[LoginIndex]() } } }

Login

jsAddToView( "var url = '" + sockJsUrl[ChatAction] + "';" + """ var socket; var initSocket = function() { socket = new SockJS(url); socket.onopen = function(event) { console.log("socket onopen", event.data); socket.send(JSON.parse({"msg":"Hello Xitrum"})); }; socket.onclose = function(event) {console.log("socket onclose", event.data);}; socket.onmessage = function(event) {console.log("socket onmessage", event.data);}; }; initSocket(); """ )

• jsAddToView/jsForView • sockJsUrl • webJarsUrl

!= jsDefaults script(src={webJarsUrl("bootstrap/3.2.0/js", "bootstrap.js", "bootstrap.min.js")}) script(src={webJarsUrl("underscorejs/1.6.0", "underscore.js", "underscore-min.js")}) != jsForView

Create chat

client with

SockJS

import xitrum.{SockJsAction, SockJsText} import xitrum.annotation.SOCKJS !@SOCKJS("connect") class ChatAction extends SockJsAction with LoginFilter { def execute() { context.become { case SockJsText(text) => SeriDeseri.fromJson[Map[String, String]](text) match { case Some(jsonMap) => // echo respondSockJsText(SeriDeseri.toJson(jsonMap)) case None => log.warn(s"Failed to parse request: $text") respondSockJsText("invalid request") } }

• SockJsAction • SockJsText/respondSockJsText • SeriDeseri.fromJson[T] / SeriDeseri.toJson(ref:AnyRef)

Create SockJsAction

(an Actor)

Lookup singleton Actor with Glokka

Xitrum

Client

ChatAction ChatAction ChatAction ChatAction

HubActor

Client Client Client

trait Hub extends Actor { protected var clients = Seq[ActorRef]() def receive = { case Subscribe(option) => clients = clients :+ sender case Unsubscribe(option) => clients = clients.filterNot(_ == sender) case Terminated(client) => clients = clients.filterNot(_ == client) case ignore => }

import glokka.Registry object Hub { val KEY_PROXY = "HUB_PROXY" val actorRegistry = Registry.start(Config.actorSystem, KEY_PROXY) } !def lookUpHub(key: String, hubProps: Props, option: Any = None) { Hub.actorRegistry ! Registry.Register(key, hubProps) context.become { case result: Registry.FoundOrCreated => result.ref ! Subscribe } }

https://github.com/xitrum-framework/glokka

hub ! Subscribe

socket open

Messaging overviewClient ChatAction HubActor

https://github.com/georgeOsdDev/glokka-demo

case class Done (option: Map[String, Any] = Map.empty) // Hub -> Action case class Publish(option: Map[String, Any] = Map.empty) // Hub -> Action case class Pull (option: Map[String, Any] = Map.empty) // Action -> Hub case class Push (option: Map[String, Any] = Map.empty) // Action -> Hub

SockJsText!(socket.send) hub ! Push(msg)

ClientChatAction

ChatAction

ChatAction

clients.foreach { _ ! Publish(msg)} respondSockJSText(msg:String)

Client

Client

sender ! Done(msg)respondSockJSText(msg:String)

Client ChatAction HubActor

SockJsText hub ! Pull(msg)

sender ! Done(msg)respondSockJSText(msg:String)

socket.onmessagesocket.onmessage

Cluster config for Hazelcast hazelcastMode = clusterMember ! cache = xitrum.hazelcast.Cache #cache { # # Simple in-memory cache # "xitrum.local.LruCache" { # maxElems = 10000 # } #} ! session { store = xitrum.hazelcast.Session # Store sessions on client side #store = xitrum.scope.session.CookieSessionStore

xitrum.conf• Xitrum-hazelcast • Shared Session • Shared Cache

akka { loggers = ["akka.event.slf4j.Slf4jLogger"] logger-startup-timeout = 30s ! actor { provider = "akka.cluster.ClusterActorRefProvider" } ! # This node remote { log-remote-lifecycle-events = off netty.tcp { hostname = "127.0.0.1" port = 2551 # 0 means random port } } ! cluster { seed-nodes = [ "akka.tcp://[email protected]:2551", "akka.tcp://[email protected]:2552"] ! auto-down-unreachable-after = 10s } }

Cluster config

for Akka

Live class reload during developmenthttps://github.com/xitrum-framework/agent7

https://github.com/dcevm/dcevm

java -javaagent:`dirname $0`/agent7-1.0.jar-XXaltjvm=dcevm -Xms256M -Xmx512M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=384M-jar `dirname $0`/sbt-launch-0.13.5.jar "$@"

https://github.com/xitrum-framework/xitrum-package

Package project for deploying to production server

sbt/sbt xitrum-package

https://github.com/xitrum-framework/xitrum-package

Monitor Xitrum in production mode

• Scalive • Metrics • Log to fluentd

https://github.com/xitrum-framework/scalive http://www.slideshare.net/georgeosd/scalive http://xitrum-framework.github.io/guide/3.18/en/log.html#log-to-fluentd

Thank you!