Download - Xitrum @ Scala Matsuri Tokyo 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.
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
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
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") × 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