Unverified Commit 40d64e1c authored by Simon Esposito's avatar Simon Esposito Committed by GitHub
Browse files

JS runtime hooks invocation fixes. (#532)

Fix invocation of InitModule function twice.
Fix leaderboard/tournament/matchmaker hooks not being invoked with the correct params.
Expose some nakama missing functions in JS runtime.
Upgrade goja.
parent 212ff0a0
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -11,6 +11,9 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr

### Fixed
- Fix an issue with the JS runtime multiUpdate function.
- Fix an issue where the JS runtime would call the InitModule function twice #530.
- Fix how the JS runtime invokes matchmakerMatched and leaderboard/tournament related hooks.
- Fix JS VM not being put back into the pool after an RPC call.

## [3.0.0] - 2021-01-16

+1 −1
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ require (
	github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect
	github.com/dgrijalva/jwt-go v3.2.1-0.20200107013213-dc14462fd587+incompatible
	github.com/dlclark/regexp2 v1.4.0 // indirect
	github.com/dop251/goja v0.0.0-20210106133455-27b0a7dc4c7f
	github.com/dop251/goja v0.0.0-20210126164150-f5884268f0c0
	github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
	github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
+2 −2
Original line number Diff line number Diff line
@@ -93,8 +93,8 @@ github.com/dgrijalva/jwt-go v3.2.1-0.20200107013213-dc14462fd587+incompatible h1
github.com/dgrijalva/jwt-go v3.2.1-0.20200107013213-dc14462fd587+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20210106133455-27b0a7dc4c7f h1:tHoCUlro23JdXvcvYXH1AseEyWJf+b8cBbVm16RYRkU=
github.com/dop251/goja v0.0.0-20210106133455-27b0a7dc4c7f/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dop251/goja v0.0.0-20210126164150-f5884268f0c0 h1:6zmaq2NhOm//MNgxDPXoLiWS1c/WTPxqqeUAQFrZRbQ=
github.com/dop251/goja v0.0.0-20210126164150-f5884268f0c0/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+63 −68
Original line number Diff line number Diff line
@@ -20,12 +20,6 @@ import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/dop251/goja"
	"github.com/gofrs/uuid"
	"github.com/golang/protobuf/jsonpb"
@@ -37,6 +31,11 @@ import (
	"go.uber.org/atomic"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const JsEntrypointFilename = "index.js"
@@ -169,6 +168,7 @@ func (rp *RuntimeProviderJS) Rpc(ctx context.Context, id string, queryParams map
	}
	fn, _ := goja.AssertFunction(r.vm.ToValue(jsFn))
	retValue, err, code := r.InvokeFunction(RuntimeExecutionModeRPC, id, fn, queryParams, userID, username, vars, expiry, sessionID, clientIP, clientPort, payload)
	rp.Put(r)
	if err != nil {
		return "", err, code
	}
@@ -246,20 +246,20 @@ func (rp *RuntimeProviderJS) AfterRt(ctx context.Context, id string, logger *zap
	jsFn := r.GetCallback(RuntimeExecutionModeAfter, id)
	if jsFn == nil {
		rp.Put(r)
		return errors.New("Runtime Before function not found.")
		return errors.New("Runtime After function not found.")
	}

	envelopeJSON, err := rp.jsonpbMarshaler.MarshalToString(envelope)
	if err != nil {
		rp.Put(r)
		logger.Error("Could not marshall envelope to JSON", zap.Any("envelope", envelope), zap.Error(err))
		return errors.New("Could not run runtime Before function.")
		return errors.New("Could not run runtime After function.")
	}
	var envelopeMap map[string]interface{}
	if err := json.Unmarshal([]byte(envelopeJSON), &envelopeMap); err != nil {
		rp.Put(r)
		logger.Error("Could not unmarshall envelope to interface{}", zap.Any("envelope_json", envelopeJSON), zap.Error(err))
		return errors.New("Could not run runtime Before function.")
		return errors.New("Could not run runtime After function.")
	}

	fn, _ := goja.AssertFunction(r.vm.ToValue(jsFn))
@@ -267,7 +267,7 @@ func (rp *RuntimeProviderJS) AfterRt(ctx context.Context, id string, logger *zap
	rp.Put(r)

	if fnErr != nil {
		logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(fnErr))
		logger.Error("Runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr))
		return fnErr
	}

@@ -451,6 +451,7 @@ func (rp *RuntimeProviderJS) Get(ctx context.Context) (*RuntimeJS, error) {
		// Context cancelled
		return nil, ctx.Err()
	case r := <-rp.poolCh:
		// Ideally use an available idle runtime.
		return r, nil
	default:
		// If there was no idle runtime, see if we can allocate a new one.
@@ -486,7 +487,7 @@ func (rp *RuntimeProviderJS) Put(r *RuntimeJS) {
	default:
		// The pool is over capacity. Should never happen but guard anyway.
		// Safe to continue processing, the runtime is just discarded.
		rp.logger.Warn("Runtime pool full, discarding Lua runtime")
		rp.logger.Warn("JavaScript runtime pool full, discarding runtime")
	}
}

@@ -1495,18 +1496,17 @@ func (rp *RuntimeProviderJS) MatchmakerMatched(ctx context.Context, entries []*M
	if err != nil {
		return "", false, err
	}
	lf := r.GetCallback(RuntimeExecutionModeMatchmaker, "")
	if lf == nil {
	jsFn := r.GetCallback(RuntimeExecutionModeMatchmaker, "")
	if jsFn == nil {
		rp.Put(r)
		return "", false, errors.New("Runtime Matchmaker Matched function not found.")
	}
	jsCtx := NewRuntimeJsContext(r.vm, r.node, r.env, RuntimeExecutionModeMatchmaker, nil, 0, "", "", nil, "", "", "")

	entriesSlice := make([]interface{}, 0, len(entries))
	for _, e := range entries {
		presenceObj := r.vm.NewObject()
		presenceObj.Set("user_id", e.Presence.UserId)
		presenceObj.Set("session_id", e.Presence.SessionId)
		presenceObj.Set("userId", e.Presence.UserId)
		presenceObj.Set("sessionId", e.Presence.SessionId)
		presenceObj.Set("username", e.Presence.Username)
		presenceObj.Set("node", e.Presence.Node)

@@ -1525,15 +1525,16 @@ func (rp *RuntimeProviderJS) MatchmakerMatched(ctx context.Context, entries []*M
		entriesSlice = append(entriesSlice, entry)
	}

	fn, _ := goja.AssertFunction(r.vm.ToValue(lf))
	retValue, err, _ := r.invokeFunction(RuntimeExecutionModeMatchmaker, "matchmakerMatched", fn, jsCtx, r.vm.ToValue(entriesSlice))
	fn, _ := goja.AssertFunction(r.vm.ToValue(jsFn))
	retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeMatchmaker, "matchmakerMatched", fn, nil, "", "", nil, 0, "", "", "", r.vm.ToValue(entriesSlice))
	rp.Put(r)

	if retValue == nil || goja.IsUndefined(retValue) || goja.IsNull(retValue) {
	if retValue == nil {
		// No return value or hook decided not to return an authoritative match ID.
		return "", false, nil
	}

	retString, ok := retValue.Export().(string)
	retString, ok := retValue.(goja.Value).Export().(string)
	if ok {
		matchIDComponents := strings.SplitN(retString, ".", 2)
		if len(matchIDComponents) != 2 {
@@ -1555,12 +1556,11 @@ func (rp *RuntimeProviderJS) TournamentEnd(ctx context.Context, tournament *api.
	if err != nil {
		return err
	}
	lf := r.GetCallback(RuntimeExecutionModeTournamentEnd, "")
	if lf == nil {
	jsFn := r.GetCallback(RuntimeExecutionModeTournamentEnd, "")
	if jsFn == nil {
		rp.Put(r)
		return errors.New("Runtime Tournament End function not found.")
	}
	jsCtx := NewRuntimeJsContext(r.vm, r.node, r.env, RuntimeExecutionModeTournamentEnd, nil, 0, "", "", nil, "", "", "")

	tournamentObj := r.vm.NewObject()
	tournamentObj.Set("id", tournament.Id)
@@ -1569,18 +1569,18 @@ func (rp *RuntimeProviderJS) TournamentEnd(ctx context.Context, tournament *api.
	tournamentObj.Set("description", tournament.Description)
	tournamentObj.Set("category", tournament.Category)
	if tournament.SortOrder == LeaderboardSortOrderAscending {
		tournamentObj.Set("sort_order", "asc")
		tournamentObj.Set("sortOrder", "asc")
	} else {
		tournamentObj.Set("sort_order", "desc")
		tournamentObj.Set("sortOrder", "desc")
	}
	tournamentObj.Set("size", tournament.Size)
	tournamentObj.Set("max_size", tournament.MaxSize)
	tournamentObj.Set("max_num_score", tournament.MaxNumScore)
	tournamentObj.Set("maxSize", tournament.MaxSize)
	tournamentObj.Set("maxNumScore", tournament.MaxNumScore)
	tournamentObj.Set("duration", tournament.Duration)
	tournamentObj.Set("start_active", tournament.StartActive)
	tournamentObj.Set("end_active", tournament.EndActive)
	tournamentObj.Set("can_enter", tournament.CanEnter)
	tournamentObj.Set("next_reset", tournament.NextReset)
	tournamentObj.Set("startActive", tournament.StartActive)
	tournamentObj.Set("endActive", tournament.EndActive)
	tournamentObj.Set("canEnter", tournament.CanEnter)
	tournamentObj.Set("nextReset", tournament.NextReset)
	metadataMap := make(map[string]interface{})
	err = json.Unmarshal([]byte(tournament.Metadata), &metadataMap)
	if err != nil {
@@ -1588,22 +1588,22 @@ func (rp *RuntimeProviderJS) TournamentEnd(ctx context.Context, tournament *api.
		return fmt.Errorf("failed to convert metadata to json: %s", err.Error())
	}
	tournamentObj.Set("metadata", metadataMap)
	tournamentObj.Set("create_time", tournament.CreateTime.Seconds)
	tournamentObj.Set("start_time", tournament.StartTime.Seconds)
	tournamentObj.Set("createTime", tournament.CreateTime.Seconds)
	tournamentObj.Set("startTime", tournament.StartTime.Seconds)
	if tournament.EndTime == nil {
		tournamentObj.Set("end_time", goja.Null())
		tournamentObj.Set("endTime", goja.Null())
	} else {
		tournamentObj.Set("end_time", tournament.EndTime.Seconds)
		tournamentObj.Set("endTime", tournament.EndTime.Seconds)
	}

	fn, _ := goja.AssertFunction(r.vm.ToValue(lf))
	retValue, err, _ := r.invokeFunction(RuntimeExecutionModeTournamentEnd, "tournamentEnd", fn, jsCtx, tournamentObj, r.vm.ToValue(end), r.vm.ToValue(reset))
	fn, _ := goja.AssertFunction(r.vm.ToValue(jsFn))
	retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeTournamentEnd, "tournamentEnd", fn, nil, "", "", nil, 0, "", "", "", tournamentObj, r.vm.ToValue(end), r.vm.ToValue(reset))
	rp.Put(r)
	if err != nil {
		return fmt.Errorf("Error running runtime Tournament End hook: %v", err.Error())
	}

	if retValue == nil || goja.IsUndefined(retValue) || goja.IsNull(retValue) {
	if retValue == nil {
		return nil
	}

@@ -1615,12 +1615,11 @@ func (rp *RuntimeProviderJS) TournamentReset(ctx context.Context, tournament *ap
	if err != nil {
		return err
	}
	lf := r.GetCallback(RuntimeExecutionModeTournamentReset, "")
	if lf == nil {
	jsFn := r.GetCallback(RuntimeExecutionModeTournamentReset, "")
	if jsFn == nil {
		rp.Put(r)
		return errors.New("Runtime Tournament Reset function not found.")
	}
	jsCtx := NewRuntimeJsContext(r.vm, r.node, r.env, RuntimeExecutionModeTournamentReset, nil, 0, "", "", nil, "", "", "")

	tournamentObj := r.vm.NewObject()
	tournamentObj.Set("id", tournament.Id)
@@ -1629,18 +1628,18 @@ func (rp *RuntimeProviderJS) TournamentReset(ctx context.Context, tournament *ap
	tournamentObj.Set("description", tournament.Description)
	tournamentObj.Set("category", tournament.Category)
	if tournament.SortOrder == LeaderboardSortOrderAscending {
		tournamentObj.Set("sort_order", "asc")
		tournamentObj.Set("sortOrder", "asc")
	} else {
		tournamentObj.Set("sort_order", "desc")
		tournamentObj.Set("sortOrder", "desc")
	}
	tournamentObj.Set("size", tournament.Size)
	tournamentObj.Set("max_size", tournament.MaxSize)
	tournamentObj.Set("max_num_score", tournament.MaxNumScore)
	tournamentObj.Set("maxSize", tournament.MaxSize)
	tournamentObj.Set("maxNumScore", tournament.MaxNumScore)
	tournamentObj.Set("duration", tournament.Duration)
	tournamentObj.Set("start_active", tournament.StartActive)
	tournamentObj.Set("end_active", tournament.EndActive)
	tournamentObj.Set("can_enter", tournament.CanEnter)
	tournamentObj.Set("next_reset", tournament.NextReset)
	tournamentObj.Set("startActive", tournament.StartActive)
	tournamentObj.Set("endActive", tournament.EndActive)
	tournamentObj.Set("canEnter", tournament.CanEnter)
	tournamentObj.Set("nextReset", tournament.NextReset)
	metadataMap := make(map[string]interface{})
	err = json.Unmarshal([]byte(tournament.Metadata), &metadataMap)
	if err != nil {
@@ -1648,23 +1647,22 @@ func (rp *RuntimeProviderJS) TournamentReset(ctx context.Context, tournament *ap
		return fmt.Errorf("failed to convert metadata to json: %s", err.Error())
	}
	tournamentObj.Set("metadata", metadataMap)
	tournamentObj.Set("create_time", tournament.CreateTime.Seconds)
	tournamentObj.Set("start_time", tournament.StartTime.Seconds)
	tournamentObj.Set("createTime", tournament.CreateTime.Seconds)
	tournamentObj.Set("startTime", tournament.StartTime.Seconds)
	if tournament.EndTime == nil {
		tournamentObj.Set("end_time", goja.Null())
		tournamentObj.Set("endTime", goja.Null())
	} else {
		tournamentObj.Set("end_time", tournament.EndTime.Seconds)
		tournamentObj.Set("endTime", tournament.EndTime.Seconds)
	}

	fn, _ := goja.AssertFunction(r.vm.ToValue(lf))
	retValue, err, _ := r.invokeFunction(RuntimeExecutionModeTournamentEnd, "tournamentReset", fn, jsCtx, tournamentObj, r.vm.ToValue(end), r.vm.ToValue(reset))

	fn, _ := goja.AssertFunction(r.vm.ToValue(jsFn))
	retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeTournamentEnd, "tournamentReset", fn, nil, "", "", nil, 0, "", "", "", tournamentObj, r.vm.ToValue(end), r.vm.ToValue(reset))
	rp.Put(r)
	if err != nil {
		return fmt.Errorf("Error running runtime Tournament Reset hook: %v", err.Error())
	}

	if retValue == nil || goja.IsUndefined(retValue) || goja.IsNull(retValue) {
	if retValue == nil {
		return nil
	}

@@ -1676,30 +1674,29 @@ func (rp *RuntimeProviderJS) LeaderboardReset(ctx context.Context, leaderboard r
	if err != nil {
		return err
	}
	lf := r.GetCallback(RuntimeExecutionModeLeaderboardReset, "")
	if lf == nil {
	jsFn := r.GetCallback(RuntimeExecutionModeLeaderboardReset, "")
	if jsFn == nil {
		rp.Put(r)
		return errors.New("Runtime Leaderboard Reset function not found.")
	}
	jsCtx := NewRuntimeJsContext(r.vm, r.node, r.env, RuntimeExecutionModeLeaderboardReset, nil, 0, "", "", nil, "", "", "")

	leaderboardObj := r.vm.NewObject()
	leaderboardObj.Set("id", leaderboard.GetId())
	leaderboardObj.Set("authoritative", leaderboard.GetAuthoritative())
	leaderboardObj.Set("sort_order", leaderboard.GetSortOrder())
	leaderboardObj.Set("sortOrder", leaderboard.GetSortOrder())
	leaderboardObj.Set("operator", leaderboard.GetOperator())
	leaderboardObj.Set("reset", leaderboard.GetReset())
	leaderboardObj.Set("metadata", leaderboard.GetMetadata())
	leaderboardObj.Set("create_time", leaderboard.GetCreateTime())
	leaderboardObj.Set("createTime", leaderboard.GetCreateTime())

	fn, _ := goja.AssertFunction(r.vm.ToValue(lf))
	retValue, err, _ := r.invokeFunction(RuntimeExecutionModeLeaderboardReset, "leaderboardReset", fn, jsCtx, leaderboardObj, r.vm.ToValue(reset))
	fn, _ := goja.AssertFunction(r.vm.ToValue(jsFn))
	retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeLeaderboardReset, "leaderboardReset", fn, nil, "", "", nil, 0, "", "", "", leaderboardObj, r.vm.ToValue(reset))
	rp.Put(r)
	if err != nil {
		return fmt.Errorf("Error running runtime Leaderboard Reset hook: %v", err.Error())
	}

	if retValue == nil || goja.IsUndefined(retValue) || goja.IsNull(retValue) {
	if retValue == nil {
		return nil
	}

@@ -1745,14 +1742,12 @@ func evalRuntimeModules(rp *RuntimeProviderJS, modCache *RuntimeJSModuleCache, m
			return nil, nil, errors.New(INIT_MODULE_FN_NAME + " function not found.")
		}

		_, err = initModFn(goja.Null(), goja.Null(), jsLoggerInst, nkInst, initializerInst)

		if dryRun {
			// Parse JavaScript code for syntax errors but do not execute the InitModule function.
			return nil, nil, nil
		}

		// Execute the init module function
		// Execute init module function
		ctx := NewRuntimeJsInitContext(r, rp.config.GetName(), rp.config.GetRuntime().Environment)
		_, err = initModFn(goja.Null(), ctx, jsLoggerInst, nkInst, initializerInst)
		if err != nil {
+1 −2
Original line number Diff line number Diff line
@@ -333,10 +333,9 @@ func (rm *RuntimeJavaScriptMatchCore) MatchLeave(tick int64, state interface{},
}

func (rm *RuntimeJavaScriptMatchCore) MatchLoop(tick int64, state interface{}, inputCh <-chan *MatchDataMessage) (interface{}, error) {
	// Drain the input queue into a Lua table.
	size := len(inputCh)
	inputs := make([]interface{}, 0, size)
	for i := 1; i <= size; i++ {
	for i := 0; i < size; i++ {
		msg := <-inputCh

		presenceObj := rm.vm.NewObject()
Loading