Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sockets: Go rewrite #3536

Closed
wants to merge 1 commit into from
Closed

Sockets: Go rewrite #3536

wants to merge 1 commit into from

Conversation

Morfent
Copy link
Contributor

@Morfent Morfent commented May 21, 2017

WIP

This turned into a pretty substantial refactor of the Node version of
sockets-related code to not only make using Go child processes
possible, but to optimize it and make unit testing of it and any code
dependent on it possible to write entirely synchronously. sockets.js
and sockets-workers.js are now written in Typescript, though they won't
be able to be transpiled until after repl.js, crashmonitor.js, Config, Users, Dnsbl, and Monitor
work with it as well.

Breaking changes:

  • this changes the requirements for the values of Config.ssl.options.cert and Config.ssl.options.key; they must be absolute paths to their corresponding files, not a buffer of their files' contents**

Fixes #2943

@Morfent Morfent force-pushed the sockets branch 3 times, most recently from 912a397 to 8837727 Compare May 21, 2017 07:15
@Morfent
Copy link
Contributor Author

Morfent commented May 27, 2017

I should probably explain the changes I made:

  • sockets.js was split into two files:
    • sockets.js, for handling logic related to being the cluster master and spawning Go and Node workers
    • sockets-workers.js for handling logic related to Node workers. I created a Multiplexer class to handle parsing downstream messages and sending them to the appropriate sockets
  • sockets.js has a GoWorker class that acts as a mock for Cluster.Worker to allow spawning a TCP server for IPC with the child process, spawning the child process, and begin handling IPC with the child process once the TCP server receives a connection from the child process
  • sockets.js has a WorkerWrapper class to wrap Node workers and GoWorker instances. This handles events common to both types of Workers, such as spawning a new worker if it crashes/gets killed and passing upstream messages to Users messages. This also allows spawning both Go and Node workers without deoptimizing Connection, User, or any other object dependent on workers to prevent methods throughout PS dependent on workers from becoming polymorphic. The only condition is Go workers must use a different port to serve with than the Node workers.
  • The sockets.js tests were rewritten to be completely synchronous using Multiplexer. They take a fraction of the time it took to run the old unit tests, since they were dependent on promises and hacky eval messages sent to child processes that had no need to be spawned in the first place.
  • pokemon-showdown verifies that Go environment variables are set, fetches dependencies if they're missing, and compiles github.com/Pokemon-Showdown/sockets/lib/, if Config.golang is set to true

On the Go side of things:

  • sockets/main.go sets up the socket multiplexer and IPC connection, launches the static and SockJS servers, and initializes the master/worker struct to allow concurrency in updating the state of the socket multiplexer.
  • sockets/lib/config.go defines the struct that represents the config settings passed from the parent proess through the $PS_CONFIG environment variable
  • sockets/lib/commands.go defines the struct (which is an abstraction of the format of messages sent over IPC) used by the socket multiplexer and IPC connection to pass to workers to be enqueued and passed to an available worker to process their payload and invoke methods on the socket multiplexer or IPC connection to process.
  • sockets/lib/ipc.go defines the struct that represents the TCP connection to the parent process. It listens for incoming messages, splits them using the DELIM character, and creates a command struct to pass to the command queue.
  • sockets/lib/master.go defines a master struct and a worker struct. This is an emulation of Node's cluster module, spawning a number of workers equal to Config.workers. A command queue receives command structs from the socket multiplexer and IPC connection; The master takes a worker's command queue out of its worker pool, allows said worker to receive a command from the command queue channel, then executes its payload. The master then takes the worker command queue and sends it back to the master's worker pool. tl;dr this implements a master/worker design pattern similar to how Node's cluster module handles concurency.
  • sockets/lib/multiplexer.go defines the struct that manages socket and worker state. *Multiplexer.Handle takes incoming sockets, stores them in *Multiplexer.sockets, then waits for incoming commands to update its state.
  • sockets/lib/*_test.go are unit tests for the files whose name precedes the _test.go part of the filename. Unit tests can be run using go test github.com/Zarel/Pokemon-Showdown/sockets/lib/; adding the -race flag will test for race conditions.

I had to use a rather hacky way to allow compiling the Go files. Rather than using go get github.com/Zarel/Pokemon-Showdown/sockets/, I made pokemon-showdown create src/github.com/Zarel/Pokemon-Showdown dirs in $GOPATH, then symlinked sockets/ to $GOPATH/src/github.com/Zarel/Pokemon-Showdown/sockets/ to prevent duplicating the rest of the files in the repo.

Copy link
Collaborator

@KamilaBorowska KamilaBorowska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job on this 👍.

const CHANNEL_REMOVE string = "-"
const CHANNEL_BROADCAST string = "#"
const SUBCHANNEL_MOVE string = "."
const SUBCHANNEL_BROADCAST string = ":"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure those could be byte instead of string. Also, they could be grouped like this.

const (
	SOCKET_CONNECT    byte = '*'
	SOCKET_DISCONNECT byte = '!'
	...
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I chose string over byte so I wouldn't have to cast command tokens to string in Command.Message, but looking over the other code dealing with these, it makes no difference which type it is. It'd make more sense for them to be byte instead

// This must be a byte that stringifies to either a hexadecimal or Unicode
// escape code. Otherwise, it would be possible for someone to send a message
// with the delimiter and break up messages.
const DELIM byte = '\u0003'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

byte is not Unicode character, you may want to use '\x03' instead to clarify (this is allowed, because its within byte bounds, but it's not exactly clear).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to make it a rune for this?

Copy link
Contributor Author

@Morfent Morfent May 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, I forgot the bufio.Reader instance requires that Reader.ReadBytes takes a byte as its delimiter

port: port,
addr: addr,
conn: conn,
listening: false}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably look better if } would be on its separate line (with trailling comma after false).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks better like this, since it doesn't take up as much space

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would extremely strongly prefer it on a separate line, but I'll defer to gofmt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gofmt has no opinion on this, and neither does Google's style guide afaik. } on a separate line it is

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, no multiline struct literal in Go style guide follows this style (singleline do, but not multiline).

(I really feel like gofmt should be able to do more than just modify whitespace, but currently it only can modify whitespace, it doesn't modify tokens in any case)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closest example I can find is this, which does follow this style https://golang.org/doc/effective_go.html#maps

@Morfent Morfent force-pushed the sockets branch 5 times, most recently from ba5a655 to 1f84f59 Compare May 28, 2017 02:44
@Morfent Morfent force-pushed the sockets branch 3 times, most recently from 4cd84e4 to 2b71e64 Compare June 7, 2017 08:35
@Morfent
Copy link
Contributor Author

Morfent commented Jun 10, 2017

Can someone take a look at the refactors I've made to GoWorker and WorkerWrapper to ensure the logic I use, specifically with GoWorker, is sound?

@Morfent
Copy link
Contributor Author

Morfent commented Jun 10, 2017

@Zarel @xfix

@Morfent Morfent force-pushed the sockets branch 7 times, most recently from 9f7b077 to 1ab65e3 Compare June 12, 2017 02:12
// themselves.
switch token {
case SOCKET_DISCONNECT:
count = 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can directly return instead of returning a variable. Also, some cases can be merged to avoid code duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand go test complains that there's no return statement at the end of the function if I return from the switch cases. I could keep count, but still merge them so the switch isn't so redundant

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. At the same time, receiving unexpected message is a bug, and probably should be reported somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it's possible if a mistake is made with /eval or if new message types are introduced.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User still should be informed about it.

"sync"
"sync/atomic"

"github.com/igm/sockjs-go/sockjs"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be using development version of SockJS (as opposed to stable gopkg.in/igm/sockjs-go.v2/sockjs)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stable version doesn't have *session.Request,yet, which is needed to get the socket's remote address and X-Forwarded-For headers to pass back to the parent process

smux sync.Mutex // nsid and sockets mutex.
channels map[string]Channel // Map of channel (i.e. room) IDs to channels.
cmux sync.Mutex // channels mutex.
scre *regexp.Regexp // Regex for splitting subchannel broadcasts into their three messages.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be part of a struct, considering scre is a constant?

Copy link
Contributor Author

@Morfent Morfent Jun 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it fits better in the struct, since only *Multiplexer.subchannelBroadcast needs to be able to use it, unlike the other constants floating around in the file's scope

listening: false,
}

return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use return syntax here, when return c, nil I think would be more clear?

Copy link
Contributor Author

@Morfent Morfent Jun 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point I was returning err instead of creating my own errors to return, so it made more sense at the time. It doesn't anymore though

type worker struct {
wpool chan chan Command // The master's pool of worker command queues.
cmdch chan Command // Queue for incoming commands from CmdQueue.
quit chan bool // Channel used to kill the worker when needed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use close on cmdch instead of making another channel just for closing purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that risk deadlocking if *master.Listen tries to send a command to worker.cmdch after it has closed?

// The multiplexer and the IPC connection both implement this interface. Its
// purpose is solely to allow the two structs to be used in Command.
type CommandIO interface {
Process(Command) (err error) // Invokes one of its methods using the command's token and parametres.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to name output variable name of interface, it's clear that error type is... an error.

}

// Final step in evaluating commands targeted at the IPC connection.
func (c *Connection) Process(cmd Command) (err error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use unnamed error, no need to use obvious output variable names.

pokemon-showdown Outdated
console.log('The GOPATH environment variable is not set! It is required in order to run the server using Go.');
process.exit(0);
}
if (!process.env.GOROOT) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOROOT is optional, and probably shouldn't be set, as its default should be fine. Don't require it, it's probably a bad idea to set that variable explicitly anyway.

Copy link
Contributor Author

@Morfent Morfent Jun 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's a bad idea to set it to something other than /usr/local/go or whatever the equivalent on Windows is, but Go will still complain about it sometimes. Maybe I'm remembering this from using older versions though. Using go env GOROOT seems like the safer option to me just to be explicit without making users having to set $GOROOT manually

pokemon-showdown Outdated
} catch (e) {}

if (config && config.golang) {
if (!process.env.GOPATH) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOPATH is optional in newer versions of Golang. You may want to assume $HOME/go path if it's not set or run go env GOPATH to obtain its location.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go env GOPATH sounds like the safer bet

pokemon-showdown Outdated
let stat;
let needsSrcDir = false;
try {
stat = fs.lstatSync(path.resolve(GOPATH, 'src/github.com/Zarel'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOPATH is allowed to be a colon separated list (semicolon on Windows), in such an event, you may want to consider its first element. You can split the list by path.delimiter

Copy link
Contributor Author

@Morfent Morfent Jun 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I wasn't aware Go would allow that. When I tested it with colon separated paths, only the first one in the list got used, so doing this seems sound

pokemon-showdown Outdated
if (!stat || !stat.isSymbolicLink()) {
try {
if (process.platform === 'win32') {
child_process.execSync(`mklink /J ${tarPath} ${srcPath}`, {stdio: 'inherit'});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs.symlinkSync

Copy link
Contributor Author

@Morfent Morfent Jun 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First time I tried that for Windows symlinks it required admin access, but I didn't realize junctions didn't at the time, which is dumb since I'm already using junctions here.

pokemon-showdown Outdated
if (process.platform === 'win32') {
child_process.execSync(`mklink /J ${tarPath} ${srcPath}`, {stdio: 'inherit'});
} else {
child_process.execSync(`ln -sF ${srcPath} ${tarPath}`, {stdio: 'inherit'});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why are you using -F, it's pointless here, as this isn't a hard link.

@KamilaBorowska
Copy link
Collaborator

KamilaBorowska commented Jun 13, 2017

Also, forgot to mention that node app, original way to start Pokemon Showdown (as opposed to npm start or ./pokemon-showdown) without dependencies already installed quietly fails (gets stuck) for some reason. Probably should just error out.

@Morfent
Copy link
Contributor Author

Morfent commented Jun 13, 2017

It doesn't kill the process for you when you try that? It does when I try it. It already throws an error when that happens, but I can change it to log the error to console and exit the process instead.

@Morfent
Copy link
Contributor Author

Morfent commented Jun 13, 2017

It also exits if config/config.jsdoesn't exist, even after having created it. I'll see if I can fix that as well,
though that'd be more appropriate to make as a pullreq apart from this one

@Morfent
Copy link
Contributor Author

Morfent commented Aug 11, 2017

It's been a month since my last commit, but I honestly can't think of any glaring issues left to fix. Is there some way to give this a test run on a reasonably active server? I still think it's too soon to be sure that this won't break on main, but any types of bugs left to find are not going to be the type I can find screwing around with a server with a dozen users.

@Zarel
Copy link
Member

Zarel commented Aug 12, 2017

Sure, rebase it and I'll test it on Smogtours.

@Morfent
Copy link
Contributor Author

Morfent commented Aug 12, 2017

Rebased

@Morfent Morfent force-pushed the sockets branch 4 times, most recently from 92aab9d to d0110b3 Compare September 17, 2017 17:33
@Morfent
Copy link
Contributor Author

Morfent commented Sep 17, 2017

...not sure why I commented and never got around to it. Now it actually is though

@Morfent Morfent force-pushed the sockets branch 2 times, most recently from 9445c21 to ddb4153 Compare September 17, 2017 18:12
@Zarel
Copy link
Member

Zarel commented Sep 17, 2017

Um, we should probably deal with the other sockets pullreq first.

@Morfent
Copy link
Contributor Author

Morfent commented Sep 19, 2017

Rebased

@Zarel
Copy link
Member

Zarel commented Sep 19, 2017

Apparently still conflicting?

@Zarel
Copy link
Member

Zarel commented Sep 19, 2017

Also: is it possible for you to pullreq into a new branch instead of directly into master?

@Morfent Morfent changed the base branch from master to sockets-go September 19, 2017 17:08
@Morfent
Copy link
Contributor Author

Morfent commented Sep 19, 2017

Alright

@Slayer95
Copy link
Contributor

[07:45] ~pre: migration to go was cancelled

@Slayer95 Slayer95 closed this Aug 15, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants