diff --git a/socketserver/server/backend.go b/socketserver/server/backend.go index 1ec4751c..e93afeab 100644 --- a/socketserver/server/backend.go +++ b/socketserver/server/backend.go @@ -62,7 +62,7 @@ func getCacheKey(remoteCommand, data string) string { return fmt.Sprintf("%s/%s", remoteCommand, data) } -// HBackendPublishRequest handles the /uncached_pub route. +// HTTPBackendUncachedPublish handles the /uncached_pub route. // The backend can POST here to publish a message to clients with no caching. // The POST arguments are `cmd`, `args`, `channel`, and `scope`. // The `scope` argument is required because no attempt is made to infer the scope from the command, unlike /cached_pub. @@ -93,7 +93,7 @@ func HTTPBackendUncachedPublish(w http.ResponseWriter, r *http.Request) { return } - cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} + cm := ClientMessage{MessageID: -1, Command: CommandPool.Intern(cmd), origArguments: json} cm.parseOrigArguments() var count int @@ -219,7 +219,7 @@ type ErrBackendNotOK struct { Code int } -// Implements the error interface. +// Error Implements the error interface. func (noe ErrBackendNotOK) Error() string { return fmt.Sprintf("backend returned %d: %s", noe.Code, noe.Response) } diff --git a/socketserver/server/commands.go b/socketserver/server/commands.go index 3f424fa5..6865d2e2 100644 --- a/socketserver/server/commands.go +++ b/socketserver/server/commands.go @@ -41,6 +41,25 @@ var commandHandlers = map[Command]CommandHandler{ "user_history": C2SHandleRemoteCommand, } +func internCommands() { + CommandPool = NewStringPool() + CommandPool._Intern_Setup(HelloCommand) + CommandPool._Intern_Setup("ping") + CommandPool._Intern_Setup(SetUserCommand) + CommandPool._Intern_Setup(ReadyCommand) + CommandPool._Intern_Setup("sub") + CommandPool._Intern_Setup("unsub") + CommandPool._Intern_Setup("track_follow") + CommandPool._Intern_Setup("emoticon_uses") + CommandPool._Intern_Setup("twitch_emote") + CommandPool._Intern_Setup("get_link") + CommandPool._Intern_Setup("get_display_name") + CommandPool._Intern_Setup("update_follow_buttons") + CommandPool._Intern_Setup("chat_history") + CommandPool._Intern_Setup("user_history") + CommandPool._Intern_Setup("adjacent_history") +} + // DispatchC2SCommand handles a C2S Command in the provided ClientMessage. // It calls the correct CommandHandler function, catching panics. // It sends either the returned Reply ClientMessage, setting the correct messageID, or sends an ErrorCommand diff --git a/socketserver/server/handlecore.go b/socketserver/server/handlecore.go index 4bbf852b..057cd456 100644 --- a/socketserver/server/handlecore.go +++ b/socketserver/server/handlecore.go @@ -60,6 +60,8 @@ var Configuration *ConfigFile var janitorsOnce sync.Once +var CommandPool StringPool + // SetupServerAndHandle starts all background goroutines and registers HTTP listeners on the given ServeMux. // Essentially, this function completely preps the server for a http.ListenAndServe call. // (Uses http.DefaultServeMux if `serveMux` is nil.) @@ -115,6 +117,7 @@ func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) { // startJanitors starts the 'is_init_func' goroutines func startJanitors() { loadUniqueUsers() + internCommands() go authorizationJanitor() go bunchCacheJanitor() @@ -508,11 +511,11 @@ func UnmarshalClientMessage(data []byte, payloadType int, v interface{}) (err er spaceIdx = strings.IndexRune(dataStr, ' ') if spaceIdx == -1 { - out.Command = Command(dataStr) + out.Command = CommandPool.Intern(dataStr) out.Arguments = nil return nil } else { - out.Command = Command(dataStr[:spaceIdx]) + out.Command = CommandPool.Intern(dataStr[:spaceIdx]) } dataStr = dataStr[spaceIdx+1:] argumentsJSON := dataStr diff --git a/socketserver/server/intern.go b/socketserver/server/intern.go new file mode 100644 index 00000000..e2eb8e1e --- /dev/null +++ b/socketserver/server/intern.go @@ -0,0 +1,37 @@ +package server + +import ( + "sync" +) + +type StringPool struct { + sync.RWMutex + lookup map[string]Command +} + +func NewStringPool() *StringPool { + return &StringPool{lookup: make(map[string]Command)} +} + +// doesn't lock, doesn't check for dupes. +func (p *StringPool) _Intern_Setup(s string) { + p.lookup[s] = Command(s) +} + +func (p *StringPool) Intern(s string) Command { + p.RLock() + ss, exists := p.lookup[s] + p.RUnlock() + if exists { + return ss + } + + p.Lock() + defer p.Unlock() + ss, exists = p.lookup[s] + if exists { + return ss + } + p.lookup[s] = Command(string([]byte(s))) + return s +} diff --git a/socketserver/server/publisher.go b/socketserver/server/publisher.go index 02843f4c..7c454a35 100644 --- a/socketserver/server/publisher.go +++ b/socketserver/server/publisher.go @@ -236,7 +236,7 @@ func HTTPBackendCachedPublish(w http.ResponseWriter, r *http.Request) { return } - cmd := Command(formData.Get("cmd")) + cmd := CommandPool.Intern(formData.Get("cmd")) json := formData.Get("args") channel := formData.Get("channel") deleteMode := formData.Get("delete") != ""