Skip to content
This repository has been archived by the owner on Oct 29, 2021. It is now read-only.

Commit

Permalink
The application's server was refactored
Browse files Browse the repository at this point in the history
Most important improvement is locking ecmascript
context for handling. To avoid race condition and
cgo crashing. Also some minor fixes were added.
  • Loading branch information
olebedev committed Aug 15, 2015
1 parent 627fa9b commit fb0a01f
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 52 deletions.
2 changes: 2 additions & 0 deletions src/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package main
import (
app "app/server"
"os"
"runtime"
)

func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
app.Run(os.Args)
}
1 change: 1 addition & 0 deletions src/app/server/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
)

// Serve the app config
func ConfHandler(c *gin.Context) {
kit := c.MustGet("kit").(*utils.Kit)
c.JSON(200, kit.Conf.Root)
Expand Down
14 changes: 10 additions & 4 deletions src/app/server/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"app/server/api"
"app/server/data"
"app/server/react"
. "app/server/utils"
"app/server/utils"

"github.com/codegangsta/cli"
"github.com/elazarl/go-bindata-assetfs"
"github.com/gin-gonic/gin"
"github.com/nu7hatch/gouuid"
)

func Run(args []string) {

app := cli.NewApp()
app.Name = "app"
app.Usage = "lmbd landing server application"
app.Usage = "React server application"

configFlag := cli.StringFlag{
Name: "config, c",
Expand All @@ -36,7 +37,7 @@ func Run(args []string) {
}

func runServer(c *cli.Context) {
kit := NewKit(c, conf)
kit := utils.NewKit(c, conf)
kit.Engine = gin.Default()

kit.Engine.StaticFS("/static", &assetfs.AssetFS{
Expand All @@ -55,6 +56,11 @@ func runServer(c *cli.Context) {
})

kit.Engine.GET("/api/v1/conf", api.ConfHandler)

kit.Engine.Use(func(c *gin.Context) {
id, _ := uuid.NewV4()
c.Set("uuid", id)
})
react.Bind(kit)
Must(kit.Engine.Run(":" + kit.Conf.UString("port")))
utils.Must(kit.Engine.Run(":" + kit.Conf.UString("port")))
}
7 changes: 0 additions & 7 deletions src/app/server/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,13 @@ local:
db: ./db.sqlite
api:
prefix: /api
duktape:
pool:
use: false
production:
debug: false
port: 5000
title: lmbd
api:
prefix: /api
duktape:
pool:
use: true
size: 1
`)
Must(err)
conf = c
Expand Down
128 changes: 88 additions & 40 deletions src/app/server/react/react.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,61 @@ package react

import (
"app/server/data"
. "app/server/utils"
"app/server/utils"
"encoding/json"
"fmt"
"net/http"
"runtime"
"time"

"github.com/gin-gonic/gin"
"github.com/nu7hatch/gouuid"
"github.com/olebedev/go-duktape"
"github.com/olebedev/go-duktape-fetch"
)

func Bind(kit *Kit) {
func Bind(kit *utils.Kit) {
r := react{kit: kit}
r.init()
kit.Engine.NoRoute(r.handle)
}

type react struct {
pool pool
kit *Kit
kit *utils.Kit
}

func (r *react) init() {
if r.kit.Conf.UBool("duktape.pool.use") {
r.pool = newDuktapePool(r.kit.Conf.UInt("duktape.pool.size", 1), r.kit.Engine)
if !r.kit.Conf.UBool("debug") {
r.pool = newDuktapePool(runtime.NumCPU(), r.kit.Engine)
} else {
// Use onDemandPool to load full react
// app each time for any http requests.
// Useful to debug the app.
r.pool = &onDemandPool{r.kit.Engine}
}
}

// Handle handles all HTTP requests which
// have no been caught via static file
// handler or other middlewares
func (r *react) handle(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
UUID := c.MustGet("uuid").(*uuid.UUID)
c.Writer.WriteHeader(http.StatusInternalServerError)
c.Writer.Header().Add("Content-Type", "text/plain")
c.Writer.Write([]byte(fmt.Sprintf("req uuid: %s\n%#v", UUID, r)))
c.Abort()
}
}()

vm := r.pool.get()
vm.Lock()

vm.PushGlobalObject()
vm.GetPropString(-1, "__router__")
// vm.Replace(-2)
vm.PushString("renderToString")

req := func() string {
Expand All @@ -47,7 +68,7 @@ func (r *react) handle(c *gin.Context) {
}()
vm.PushString(req)
vm.JsonDecode(-1)
ch := make(chan struct{}, 1)
ch := make(chan *resp, 1)
vm.PushGoFunction(func(ctx *duktape.Context) int {

// Getting response object via json
Expand All @@ -57,40 +78,56 @@ func (r *react) handle(c *gin.Context) {
return &re
}()

// Unlock handler
ch <- r
// Return nothing into duktape context
return 0
})
vm.PcallProp(1, 2)
vm.Unlock()

// Lock handler and wait for js app response
select {
case re := <-ch:
// Hold the context. This call is really important
// because async calls is possible. So, we cannot
// allow to break the context stack.
vm.Lock()
// Clean duktape vm stack
vm.PopN(vm.GetTop())

// Drop any futured async calls
vm.ResetTimers()
// Release the context
vm.Unlock()
// Return vm back to the pool
r.pool.put(vm)

// Handle the response
if len(r.Redirect) == 0 && len(r.Error) == 0 {
if len(re.Redirect) == 0 && len(re.Error) == 0 {
// If no redirection and no error
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Header().Add("Content-Type", "text/html")
c.Writer.Write([]byte("<!doctype html>\n" + r.Body))
c.Writer.Write([]byte("<!doctype html>\n" + re.Body))
c.Abort()
// If redirect
} else if len(r.Redirect) != 0 {
c.Redirect(http.StatusMovedPermanently, r.Redirect)
} else if len(re.Redirect) != 0 {
c.Redirect(http.StatusMovedPermanently, re.Redirect)
// If internal error
} else if len(r.Error) != 0 {
} else if len(re.Error) != 0 {
c.Writer.WriteHeader(http.StatusInternalServerError)
c.Writer.Header().Add("Content-Type", "text/plain")
c.Writer.Write([]byte(r.Error))
c.Writer.Write([]byte(re.Error))
c.Abort()
}

// Unlock handler
ch <- struct{}{}
// Return nothing into duktape context
return 0
})

// Duktape stack -> [ {global}, __router__, "renderToString", {\"url\":\"...\"}, {url:...}, {func: true} ]
vm.PcallProp(1, 2)
// Lock handler and wait for app response
<-ch
// Clean stack
if i := vm.GetTop(); i > 0 {
vm.PopN(i)
case <-time.After(5 * time.Second):
r.pool.drop(vm)
UUID := c.MustGet("uuid").(*uuid.UUID)
c.Writer.WriteHeader(http.StatusInternalServerError)
c.Writer.Header().Add("Content-Type", "text/plain")
c.Writer.Write([]byte(fmt.Sprintf("req uuid: %s\ntime is out", UUID)))
c.Abort()
}
// Return vm back to the pool
r.pool.put(vm)
}

type resp struct {
Expand All @@ -103,20 +140,21 @@ type resp struct {
type pool interface {
get() *duktape.Context
put(*duktape.Context)
drop(*duktape.Context)
}

func newDuktapePool(size int, engine *gin.Engine) *duktapePool {
pool := &duktapePool{
ch: make(chan *duktape.Context, size),
ch: make(chan *duktape.Context, size),
engine: engine,
}
loop:
for {
select {
case pool.ch <- newDuktapeContext(engine):
default:
break loop

go func() {
for i := 0; i < size; i++ {
pool.ch <- newDuktapeContext(engine)
}
}
}()

return pool
}

Expand All @@ -126,7 +164,7 @@ func newDuktapeContext(engine *gin.Engine) *duktape.Context {
vm.PevalString(`var console = {log:print,warn:print,error:print,info:print}`)
fetch.Define(vm, engine)
app, err := data.Asset("static/build/bundle.js")
Must(err)
utils.Must(err)
fmt.Println("static/build/bundle.js loaded")
if err := vm.PevalString(string(app)); err != nil {
derr := err.(*duktape.Error)
Expand All @@ -137,7 +175,6 @@ func newDuktapeContext(engine *gin.Engine) *duktape.Context {
return vm
}

// Loads file pre request
type onDemandPool struct {
engine *gin.Engine
}
Expand All @@ -151,15 +188,26 @@ func (_ onDemandPool) put(c *duktape.Context) {
c.DestroyHeap()
}

// Prefetched pool
func (on *onDemandPool) drop(c *duktape.Context) {
on.put(c)
}

type duktapePool struct {
ch chan *duktape.Context
ch chan *duktape.Context
engine *gin.Engine
}

func (o *duktapePool) get() *duktape.Context {
return <-o.ch
}

func (o *duktapePool) put(ot *duktape.Context) {
ot.Gc(0)
o.ch <- ot
}

func (o *duktapePool) drop(ot *duktape.Context) {
ot.DestroyHeap()
ot = nil
o.ch <- newDuktapeContext(o.engine)
}
3 changes: 2 additions & 1 deletion src/app/server/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ type Kit struct {

func NewKit(c *cli.Context, conf *config.Config) *Kit {
co, err := conf.Get(c.String("config"))
co.Env() // parse environ variables
// parse environ variables
co.Env()
Must(err)

// set up gin
Expand Down

0 comments on commit fb0a01f

Please sign in to comment.