Greatly reduced the number of w.Write calls in the generated templates.

The topic list should only be rebuilt for a short window after something related to it changes on single server setups.
Swapped out the RebuildGroupPermissions call in UpdatePerms with a more general and versable Groups.Reload call.
The template generator now use log instead of fmt for writing debug logs.
This commit is contained in:
Azareal 2018-11-20 09:06:15 +10:00
parent f508ef9898
commit 1aac6f1268
14 changed files with 236 additions and 85 deletions

View File

@ -155,6 +155,7 @@ func (fps *MemoryForumPermsStore) Reload(fid int) error {
}
DebugDetailf("group.CanSee (length %d): %+v \n", len(group.CanSee), group.CanSee)
}
TopicListThaw.Thaw()
return nil
}

View File

@ -121,6 +121,7 @@ func (mfs *MemoryForumStore) LoadForums() error {
addForum(forum)
}
mfs.forumView.Store(forumView)
TopicListThaw.Thaw()
return rows.Err()
}
@ -185,6 +186,7 @@ func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) {
forum.Link = BuildForumURL(NameToSlug(forum.Name), forum.ID)
forum.LastTopic = Topics.DirtyGet(forum.LastTopicID)
forum.LastReplyer = Users.DirtyGet(forum.LastReplyerID)
TopicListThaw.Thaw()
return forum, err
}
@ -213,6 +215,7 @@ func (mfs *MemoryForumStore) Reload(id int) error {
forum.LastReplyer = Users.DirtyGet(forum.LastReplyerID)
mfs.CacheSet(forum)
TopicListThaw.Thaw()
return nil
}
@ -283,6 +286,7 @@ func (mfs *MemoryForumStore) Delete(id int) error {
}
_, err := mfs.delete.Exec(id)
mfs.CacheDelete(id)
TopicListThaw.Thaw()
return err
}

View File

@ -58,7 +58,6 @@ func (group *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err err
if err != nil {
return err
}
Groups.Reload(group.ID)
return nil
}
@ -68,7 +67,6 @@ func (group *Group) Update(name string, tag string) (err error) {
if err != nil {
return err
}
Groups.Reload(group.ID)
return nil
}
@ -83,7 +81,7 @@ func (group *Group) UpdatePerms(perms map[string]bool) (err error) {
if err != nil {
return err
}
return RebuildGroupPermissions(group.ID)
return Groups.Reload(group.ID)
}
// Copy gives you a non-pointer concurrency safe copy of the group

View File

@ -90,6 +90,7 @@ func (mgs *MemoryGroupStore) LoadGroups() error {
DebugLog("Binding the Not Loggedin Group")
GuestPerms = mgs.dirtyGetUnsafe(6).Perms
TopicListThaw.Thaw()
return nil
}
@ -152,6 +153,7 @@ func (mgs *MemoryGroupStore) Reload(id int) error {
if err != nil {
LogError(err)
}
TopicListThaw.Thaw()
return nil
}
@ -280,6 +282,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
mgs.groupCount++
mgs.Unlock()
TopicListThaw.Thaw()
return gid, FPStore.ReloadAll()
//return gid, TopicList.RebuildPermTree()
}

View File

@ -1,14 +1,22 @@
package tmpl
import (
"errors"
"reflect"
)
type Fragment struct {
Body string
TemplateName string
Index int
Seen bool
}
type OutBufferFrame struct {
Body string
Type string
TemplateName string
Extra interface{}
Extra2 interface{}
}
type CContext struct {
@ -18,39 +26,12 @@ type CContext struct {
OutBuf *[]OutBufferFrame
}
func (con *CContext) Push(nType string, body string) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, nType, con.TemplateName})
func (con *CContext) Push(nType string, body string) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, nType, con.TemplateName, nil, nil})
return len(*con.OutBuf) - 1
}
func (con *CContext) GetLastType() string {
outBuf := *con.OutBuf
if len(outBuf) == 0 {
return ""
}
return outBuf[len(outBuf)-1].Type
}
func (con *CContext) GetLastBody() string {
outBuf := *con.OutBuf
if len(outBuf) == 0 {
return ""
}
return outBuf[len(outBuf)-1].Body
}
func (con *CContext) SetLastBody(newBody string) error {
outBuf := *con.OutBuf
if len(outBuf) == 0 {
return errors.New("outbuf is empty")
}
outBuf[len(outBuf)-1].Body = newBody
return nil
}
func (con *CContext) GetLastTemplate() string {
outBuf := *con.OutBuf
if len(outBuf) == 0 {
return ""
}
return outBuf[len(outBuf)-1].TemplateName
func (con *CContext) PushText(body string, fragIndex int, fragOutIndex int) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "text", con.TemplateName, fragIndex, fragOutIndex})
return len(*con.OutBuf) - 1
}

View File

@ -40,12 +40,6 @@ type CTemplateConfig struct {
PackageName string
}
type Fragment struct {
Body string
TemplateName string
Index int
}
// nolint
type CTemplateSet struct {
templateList map[string]*parse.Tree
@ -53,7 +47,7 @@ type CTemplateSet struct {
funcMap map[string]interface{}
importMap map[string]string
TemplateFragmentCount map[string]int
Fragments map[string]int
FragOnce map[string]bool
fragmentCursor map[string]int
FragOut string
fragBuf []Fragment
@ -122,6 +116,16 @@ func (c *CTemplateSet) SetBuildTags(tags string) {
c.buildTags = tags
}
type SkipBlock struct {
Frags map[int]int
LastCount int
ClosestFragSkip int
}
type Skipper struct {
Count int
Index int
}
func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
if c.config.Debug {
fmt.Println("Compiling template '" + name + "'")
@ -173,10 +177,11 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
c.localVars = make(map[string]map[string]VarItemReflect)
c.localVars[fname] = make(map[string]VarItemReflect)
c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect}
if c.Fragments == nil {
c.Fragments = make(map[string]int)
if c.FragOnce == nil {
c.FragOnce = make(map[string]bool)
}
c.fragmentCursor = map[string]int{fname: 0}
c.fragBuf = nil
c.langIndexToName = nil
// TODO: Is this the first template loaded in? We really should have some sort of constructor for CTemplateSet
@ -187,6 +192,11 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
c.rootIterate(c.templateList[fname], con)
c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1
_, ok := c.FragOnce[fname]
if !ok {
c.FragOnce[fname] = true
}
if len(c.langIndexToName) > 0 {
c.importMap[langPkg] = langPkg
}
@ -195,7 +205,6 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
for _, item := range c.importMap {
importList += "import \"" + item + "\"\n"
}
var varString string
for _, varItem := range c.varList {
varString += "var " + varItem.Name + " " + varItem.Type + " = " + varItem.Destination + "\n"
@ -205,7 +214,6 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
if c.buildTags != "" {
fout += "// +build " + c.buildTags + "\n\n"
}
fout += "// Code generated by Gosora. More below:\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n"
fout += "package " + c.config.PackageName + "\n" + importList + "\n"
@ -238,20 +246,74 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
fout += "var plist = phrases.GetTmplPhrasesBytes(" + fname + "_tmpl_phrase_id)\n"
}
fout += varString
for _, frame := range outBuf {
var skipped = make(map[string]*SkipBlock) // map[templateName]*SkipBlock{map[atIndexAndAfter]skipThisMuch,lastCount}
var writeTextFrame = func(tmplName string, index int) {
out := "w.Write(" + tmplName + "_frags[" + strconv.Itoa(index) + "]" + ")\n"
c.detail("writing ", out)
fout += out
}
for fid := 0; len(outBuf) > fid; fid++ {
frame := outBuf[fid]
if frame.Type == "text" {
c.detail("text frame:")
c.detail(frame)
oid := fid
skipBlock, ok := skipped[frame.TemplateName]
if !ok {
skipBlock = &SkipBlock{make(map[int]int), 0, 0}
skipped[frame.TemplateName] = skipBlock
}
skip := skipBlock.LastCount
c.detailf("skipblock %+v\n", skipBlock)
for len(outBuf) > fid+1 && outBuf[fid+1].Type == "text" && outBuf[fid+1].TemplateName == frame.TemplateName {
next := outBuf[fid+1]
c.detail("next frame:", next)
c.detail("frame frag:", c.fragBuf[frame.Extra2.(int)])
c.detail("next frag:", c.fragBuf[next.Extra2.(int)])
c.fragBuf[frame.Extra2.(int)].Body += c.fragBuf[next.Extra2.(int)].Body
c.fragBuf[next.Extra2.(int)].Seen = true
fid++
skipBlock.LastCount += (fid - oid)
skipBlock.Frags[frame.Extra.(int)] = skipBlock.LastCount
}
writeTextFrame(frame.TemplateName, frame.Extra.(int)-skip)
} else {
c.detail(frame.Type + " frame")
fout += frame.Body
}
}
fout += "return nil\n}\n"
var writeFrag = func(tmplName string, index int, body string) {
fragmentPrefix := tmplName + "_frags[" + strconv.Itoa(index) + "]" + " = []byte(`" + body + "`)\n"
c.detail("writing ", fragmentPrefix)
c.FragOut += fragmentPrefix
}
for _, frag := range c.fragBuf {
c.detail("frag: ", frag)
if frag.Seen {
c.detail("invisible")
continue
}
skipBlock := skipped[frag.TemplateName]
skip := skipBlock.Frags[skipBlock.ClosestFragSkip]
_, ok := skipBlock.Frags[frag.Index]
if ok {
skipBlock.ClosestFragSkip = frag.Index
}
c.detailf("skipblock %+v\n", skipBlock)
c.detail("skipping ", skip)
writeFrag(frag.TemplateName, frag.Index-skip, frag.Body)
}
fout = strings.Replace(fout, `))
w.Write([]byte(`, " + ", -1)
fout = strings.Replace(fout, "` + `", "", -1)
for _, frag := range c.fragBuf {
fragmentPrefix := frag.TemplateName + "_frags[" + strconv.Itoa(frag.Index) + "]"
c.FragOut += fragmentPrefix + " = []byte(`" + frag.Body + "`)\n"
}
if c.config.Debug {
for index, count := range c.stats {
fmt.Println(index+": ", strconv.Itoa(count))
@ -264,6 +326,7 @@ w.Write([]byte(`, " + ", -1)
}
func (c *CTemplateSet) rootIterate(tree *parse.Tree, con CContext) {
c.dumpCall("rootIterate", tree, con)
c.detail(tree.Root)
treeLength := len(tree.Root.Nodes)
for index, node := range tree.Root.Nodes {
@ -275,10 +338,12 @@ func (c *CTemplateSet) rootIterate(tree *parse.Tree, con CContext) {
}
c.compileSwitch(con, node)
}
c.retCall("rootIterate")
}
func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
c.dumpCall("compileSwitch", con, node)
defer c.retCall("compileSwitch")
switch node := node.(type) {
case *parse.ActionNode:
c.detail("Action Node")
@ -333,15 +398,12 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
return
}
fragmentName := con.TemplateName + "_" + strconv.Itoa(c.fragmentCursor[con.TemplateName])
fragmentPrefix := con.TemplateName + "_frags[" + strconv.Itoa(c.fragmentCursor[con.TemplateName]) + "]"
_, ok := c.Fragments[fragmentName]
if !ok {
c.Fragments[fragmentName] = len(node.Text)
c.fragBuf = append(c.fragBuf, Fragment{string(node.Text), con.TemplateName, c.fragmentCursor[con.TemplateName]})
}
c.fragmentCursor[con.TemplateName] = c.fragmentCursor[con.TemplateName] + 1
con.Push("text", "w.Write("+fragmentPrefix+")\n")
nodeText := string(node.Text)
fragIndex := c.fragmentCursor[con.TemplateName]
_, ok := c.FragOnce[con.TemplateName]
c.fragBuf = append(c.fragBuf, Fragment{nodeText, con.TemplateName, fragIndex, ok})
con.PushText(strconv.Itoa(fragIndex), fragIndex, len(c.fragBuf)-1)
c.fragmentCursor[con.TemplateName] = fragIndex + 1
default:
c.unknownNode(node)
}
@ -349,6 +411,7 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
c.dumpCall("compileRangeNode", con, node)
defer c.retCall("compileRangeNode")
c.detail("node.Pipe: ", node.Pipe)
var expr string
var outVal reflect.Value
@ -653,8 +716,11 @@ func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNod
func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal bool) {
c.dumpCall("compileIdentSwitch", con, node)
var litString = func(inner string) {
out = "w.Write([]byte(" + inner + "))\n"
var litString = func(inner string, bytes bool) {
if !bytes {
inner = "[]byte(" + inner + ")"
}
out = "w.Write(" + inner + ")\n"
literal = true
}
ArgLoop:
@ -682,7 +748,7 @@ ArgLoop:
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
// TODO: Validate that this is actually a time.Time
litString("time.Since(" + leftParam + ").String()")
litString("time.Since("+leftParam+").String()", false)
c.importMap["time"] = "time"
break ArgLoop
case "dock":
@ -692,7 +758,6 @@ ArgLoop:
if len(leftOperand) == 0 || len(rightOperand) == 0 {
panic("The left or right operand for function dock cannot be left blank")
}
leftParam := leftOperand
if leftOperand[0] != '"' {
leftParam, _ = c.compileIfVarSub(con, leftParam)
@ -707,7 +772,7 @@ ArgLoop:
val = val3
// TODO: Refactor this
litString("common.BuildWidget(" + leftParam + "," + rightParam + ")")
litString("common.BuildWidget("+leftParam+","+rightParam+")", false)
break ArgLoop
case "lang":
// TODO: Implement string literals properly
@ -718,11 +783,10 @@ ArgLoop:
if leftOperand[0] != '"' {
panic("Phrase names cannot be dynamic")
}
// ! Slightly crude but it does the job
leftParam := strings.Replace(leftOperand, "\"", "", -1)
c.langIndexToName = append(c.langIndexToName, leftParam)
litString("plist[" + strconv.Itoa(len(c.langIndexToName)-1) + "]")
litString("plist["+strconv.Itoa(len(c.langIndexToName)-1)+"]", true)
break ArgLoop
case "level":
// TODO: Implement level literals
@ -732,7 +796,7 @@ ArgLoop:
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString("phrases.GetLevelPhrase(" + leftParam + ")")
litString("phrases.GetLevelPhrase("+leftParam+")", false)
c.importMap[langPkg] = langPkg
break ArgLoop
case "scope":
@ -991,6 +1055,7 @@ func (c *CTemplateSet) retCall(name string, params ...interface{}) {
func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.Value, assLines string, onEnd func(string) string) {
c.dumpCall("compileVarSub", con, varname, val, assLines, onEnd)
defer c.retCall("compileVarSub")
if onEnd == nil {
onEnd = func(in string) string {
return in
@ -999,7 +1064,7 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
// Is this a literal string?
if len(varname) != 0 && varname[0] == '"' {
con.Push("varsub", onEnd(assLines+"w.Write([]byte("+varname+"))\n"))
con.Push("lvarsub", onEnd(assLines+"w.Write([]byte("+varname+"))\n"))
return
}
for _, varItem := range c.varList {
@ -1030,17 +1095,23 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
switch val.Kind() {
case reflect.Int:
c.importMap["strconv"] = "strconv"
base = "w.Write([]byte(strconv.Itoa(" + varname + ")))\n"
base = "[]byte(strconv.Itoa(" + varname + "))"
case reflect.Bool:
base = "if " + varname + " {\nw.Write([]byte(\"true\"))} else {\nw.Write([]byte(\"false\"))\n}\n"
con.Push("startif", "if "+varname+" {\n")
con.Push("varsub", "w.Write([]byte(\"true\"))")
con.Push("endif", "} ")
con.Push("startelse", "else {\n")
con.Push("varsub", "w.Write([]byte(\"false\"))")
con.Push("endelse", "}\n")
return
case reflect.String:
if val.Type().Name() != "string" && !strings.HasPrefix(varname, "string(") {
varname = "string(" + varname + ")"
}
base = "w.Write([]byte(" + varname + "))\n"
base = "[]byte(" + varname + ")"
case reflect.Int64:
c.importMap["strconv"] = "strconv"
base = "w.Write([]byte(strconv.FormatInt(" + varname + ", 10)))\n"
base = "[]byte(strconv.FormatInt(" + varname + ", 10))"
default:
if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
@ -1050,12 +1121,17 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
fmt.Println("Unknown Type:", val.Type().Name())
panic("-- I don't know what this variable's type is o.o\n")
}
base = "w.Write(" + base + ")\n"
c.detail("base: ", base)
con.Push("varsub", onEnd(assLines+base))
if assLines == "" {
con.Push("varsub", base)
} else {
con.Push("lvarsub", onEnd(assLines+base))
}
}
func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNode) {
c.debugCall("compileSubTemplate", pcon, node)
c.dumpCall("compileSubTemplate", pcon, node)
c.detail("Template Node: ", node.Name)
fname := strings.TrimSuffix(node.Name, filepath.Ext(node.Name))
@ -1107,26 +1183,33 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
c.localVars[fname] = make(map[string]VarItemReflect)
c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect}
c.fragmentCursor[fname] = 0
con.Push("starttemplate", "{\n")
c.rootIterate(subtree, con)
con.Push("endtemplate", "}\n")
c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1
_, ok := c.FragOnce[fname]
if !ok {
c.FragOnce[fname] = true
}
}
// TODO: Should we rethink the way the log methods work or their names?
func (c *CTemplateSet) detail(args ...interface{}) {
if c.config.SuperDebug {
fmt.Println(args...)
log.Println(args...)
}
}
func (c *CTemplateSet) detailf(left string, args ...interface{}) {
if c.config.SuperDebug {
fmt.Printf(left, args...)
log.Printf(left, args...)
}
}
func (c *CTemplateSet) error(args ...interface{}) {
if c.config.Debug {
fmt.Println(args...)
log.Println(args...)
}
}

70
common/thaw.go Normal file
View File

@ -0,0 +1,70 @@
package common
import (
"sync"
"sync/atomic"
)
var TopicListThaw ThawInt
type ThawInt interface {
Thawed() bool
Thaw()
Tick() error
}
type SingleServerThaw struct {
DefaultThaw
}
func NewSingleServerThaw() *SingleServerThaw {
thaw := &SingleServerThaw{}
if Config.ServerCount == 1 {
AddScheduledSecondTask(thaw.Tick)
}
return thaw
}
func (thaw *SingleServerThaw) Thawed() bool {
if Config.ServerCount == 1 {
return thaw.DefaultThaw.Thawed()
}
return true
}
func (thaw *SingleServerThaw) Thaw() {
if Config.ServerCount == 1 {
thaw.DefaultThaw.Thaw()
}
}
type DefaultThaw struct {
thawed int64
sync.Mutex
}
func NewDefaultThaw() *DefaultThaw {
thaw := &DefaultThaw{}
AddScheduledSecondTask(thaw.Tick)
return thaw
}
// Decrement the thawed counter once a second until it goes cold
func (thaw *DefaultThaw) Tick() error {
thaw.Lock()
defer thaw.Unlock()
prior := thaw.thawed
if prior > 0 {
atomic.StoreInt64(&thaw.thawed, prior-1)
}
return nil
}
func (thaw *DefaultThaw) Thawed() bool {
return thaw.thawed > 0
}
func (thaw *DefaultThaw) Thaw() {
atomic.StoreInt64(&thaw.thawed, 5)
}

View File

@ -192,6 +192,7 @@ func (topic *Topic) cacheRemove() {
if tcache != nil {
tcache.Remove(topic.ID)
}
TopicListThaw.Thaw()
}
// TODO: Write a test for this
@ -259,6 +260,7 @@ func (topic *Topic) Like(score int, uid int) (err error) {
// TODO: Implement this
func (topic *Topic) Unlike(uid int) error {
topic.cacheRemove()
return nil
}

View File

@ -52,6 +52,12 @@ func NewDefaultTopicList() (*DefaultTopicList, error) {
}
func (tList *DefaultTopicList) Tick() error {
//fmt.Println("TopicList.Tick")
if !TopicListThaw.Thawed() {
return nil
}
//fmt.Println("building topic list")
var oddLists = make(map[int]*TopicListHolder)
var evenLists = make(map[int]*TopicListHolder)

View File

@ -113,6 +113,7 @@ func (mts *DefaultTopicStore) Reload(id int) error {
} else {
_ = mts.cache.Remove(id)
}
TopicListThaw.Thaw()
return err
}

View File

@ -181,6 +181,7 @@ func (user *User) CacheRemove() {
if ucache != nil {
ucache.Remove(user.ID)
}
TopicListThaw.Thaw()
}
func (user *User) Ban(duration time.Duration, issuedBy int) error {

View File

@ -60,7 +60,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""),
register: acc.Insert("users").Columns("name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt, lastLiked, oldestItemLikedCreatedAt").Fields("?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), // TODO: Implement user_count on users_groups here
usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""),
userCount: acc.SimpleCount("users", "", ""),
userCount: acc.Count("users").Prepare(),
}, acc.FirstError()
}
@ -244,6 +244,7 @@ func (mus *DefaultUserStore) Reload(id int) error {
user.Init()
_ = mus.cache.Set(user)
TopicListThaw.Thaw()
return nil
}
@ -270,12 +271,10 @@ func (mus *DefaultUserStore) Create(username string, password string, email stri
if err != ErrNoRows {
return 0, ErrAccountExists
}
salt, err := GenerateSafeString(SaltLength)
if err != nil {
return 0, err
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password+salt), bcrypt.DefaultCost)
if err != nil {
return 0, err

View File

@ -69,6 +69,7 @@ func gloinit() (err error) {
if err != nil {
return errors.WithStack(err)
}
common.TopicListThaw = common.NewSingleServerThaw()
common.SwitchToTestDB()
var ok bool

View File

@ -259,6 +259,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
common.TopicListThaw = common.NewSingleServerThaw()
err = InitDatabase()
if err != nil {