Parallel start/stop/reload systems, for testing

This commit is contained in:
Griatch 2018-01-14 14:02:34 +01:00
parent ff887a07ab
commit 5741eef9bc
5 changed files with 109 additions and 39 deletions

View file

@ -58,7 +58,7 @@ class CmdReload(COMMAND_DEFAULT_CLASS):
if self.args: if self.args:
reason = "(Reason: %s) " % self.args.rstrip(".") reason = "(Reason: %s) " % self.args.rstrip(".")
SESSIONS.announce_all(" Server restart initiated %s..." % reason) SESSIONS.announce_all(" Server restart initiated %s..." % reason)
SESSIONS.server.shutdown(mode='reload') SESSIONS.portal_restart_server()
class CmdReset(COMMAND_DEFAULT_CLASS): class CmdReset(COMMAND_DEFAULT_CLASS):
@ -91,7 +91,7 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
Reload the system. Reload the system.
""" """
SESSIONS.announce_all(" Server resetting/restarting ...") SESSIONS.announce_all(" Server resetting/restarting ...")
SESSIONS.server.shutdown(mode='reset') SESSIONS.portal_reset_server()
class CmdShutdown(COMMAND_DEFAULT_CLASS): class CmdShutdown(COMMAND_DEFAULT_CLASS):

View file

@ -96,6 +96,16 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
""" """
# sending AMP data # sending AMP data
def connectionMade(self):
"""
Called when a new connection is established.
"""
super(AMPServerClientProtocol, self).connectionMade()
# first thing we do is to request the Portal to sync all sessions
# back with the Server side
self.send_AdminServer2Portal(amp.DUMMYSESSION, operation=amp.PSYNC)
def send_MsgServer2Portal(self, session, **kwargs): def send_MsgServer2Portal(self, session, **kwargs):
""" """
Access method - executed on the Server for sending data Access method - executed on the Server for sending data
@ -118,7 +128,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
operation (char, optional): Identifier for the server operation (char, optional): Identifier for the server
operation, as defined by the global variables in operation, as defined by the global variables in
`evennia/server/amp.py`. `evennia/server/amp.py`.
data (str or dict, optional): Data going into the adminstrative. kwargs (dict, optional): Data going into the adminstrative.
""" """
return self.data_out(amp.AdminServer2Portal, session.sessid, operation=operation, **kwargs) return self.data_out(amp.AdminServer2Portal, session.sessid, operation=operation, **kwargs)
@ -180,12 +190,17 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
server_sessionhandler.portal_disconnect_all() server_sessionhandler.portal_disconnect_all()
elif operation == amp.PSYNC: # portal_session_sync elif operation == amp.PSYNC: # portal_session_sync
# force a resync of sessions when portal reconnects to # force a resync of sessions from the portal side
# server (e.g. after a server reboot) the data kwarg
# contains a dict {sessid: {arg1:val1,...}}
# representing the attributes to sync for each
# session.
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
elif operation == amp.SRELOAD: # server reload
# shut down in reload mode
server_sessionhandler.server.shutdown(mode='reload')
elif operation == amp.SRESET:
# shut down in reset mode
server_sessionhandler.server.shutdown(mode='reset')
elif operation == amp.SSHUTD: # server shutdown
# shutdown in stop mode
server_sessionhandler.server.shutdown(mode='shutdown')
else: else:
raise Exception("operation %(op)s not recognized." % {'op': operation}) raise Exception("operation %(op)s not recognized." % {'op': operation})
return {} return {}

View file

@ -37,11 +37,12 @@ SSYNC = chr(8) # server session sync
SCONN = chr(11) # server creating new connection (for irc bots and etc) SCONN = chr(11) # server creating new connection (for irc bots and etc)
PCONNSYNC = chr(12) # portal post-syncing a session PCONNSYNC = chr(12) # portal post-syncing a session
PDISCONNALL = chr(13) # portal session disconnect all PDISCONNALL = chr(13) # portal session disconnect all
SRELOAD = chr(14) # server reloading (have portal start a new server) SRELOAD = chr(14) # server shutdown in reload mode
SSTART = chr(15) # server start (portal must already be running anyway) SSTART = chr(15) # server start (portal must already be running anyway)
PSHUTD = chr(16) # portal (+server) shutdown PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server-only shutdown SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed) AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
BATCH_RATE = 250 # max commands/sec before switching to batch-sending BATCH_RATE = 250 # max commands/sec before switching to batch-sending
@ -271,9 +272,22 @@ class AMPMultiConnectionProtocol(amp.AMP):
else: else:
super(AMPMultiConnectionProtocol, self).dataReceived(data) super(AMPMultiConnectionProtocol, self).dataReceived(data)
def makeConnection(self, transport):
"""
Swallow connection log message here. Copied from original
in the amp protocol.
"""
# copied from original, removing the log message
if not self._ampInitialized:
amp.AMP.__init__(self)
self._transportPeer = transport.getPeer()
self._transportHost = transport.getHost()
amp.BinaryBoxProtocol.makeConnection(self, transport)
def connectionMade(self): def connectionMade(self):
""" """
This is called when an AMP connection is (re-)established AMP calls it on both sides. This is called when an AMP connection is (re-)established. AMP calls it on both sides.
""" """
self.factory.broadcasts.append(self) self.factory.broadcasts.append(self)

View file

@ -9,6 +9,7 @@ import sys
from twisted.internet import protocol from twisted.internet import protocol
from evennia.server.portal import amp from evennia.server.portal import amp
from subprocess import Popen from subprocess import Popen
from evennia.utils import logger
def getenv(): def getenv():
@ -69,20 +70,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
Protocol subclass for the AMP-server run by the Portal. Protocol subclass for the AMP-server run by the Portal.
""" """
def connectionMade(self):
"""
Called when a new connection is established.
"""
super(AMPServerProtocol, self).connectionMade()
if len(self.factory.broadcasts) < 2:
sessdata = self.factory.portal.sessions.get_all_sync_data()
self.send_AdminPortal2Server(amp.DUMMYSESSION,
amp.PSYNC,
sessiondata=sessdata)
self.factory.portal.sessions.at_server_connection()
def start_server(self, server_twistd_cmd): def start_server(self, server_twistd_cmd):
""" """
(Re-)Launch the Evennia server. (Re-)Launch the Evennia server.
@ -92,10 +79,29 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
to pass to POpen to start the server. to pass to POpen to start the server.
""" """
# start the server # start the Server
process = Popen(server_twistd_cmd, env=getenv()) process = Popen(server_twistd_cmd, env=getenv())
# store the pid for future reference # store the pid for future reference
self.portal.server_process_id = process.pid self.factory.portal.server_process_id = process.pid
self.factory.portal.server_twistd_cmd = server_twistd_cmd
return process.pid
def stop_server(self, mode='reload'):
"""
Shut down server in one or more modes.
Args:
mode (str): One of 'shutdown', 'reload' or 'reset'.
"""
if mode == 'reload':
self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SRELOAD)
return self.start_server(self.factory.portal.server_twistd_cmd)
if mode == 'reset':
self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SRESET)
return self.start_server(self.factory.portal.server_twistd_cmd)
if mode == 'shutdown':
self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SSHUTD)
# sending amp data # sending amp data
@ -173,28 +179,44 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
launcher. It can obviously only accessed when the Portal is already up and running. launcher. It can obviously only accessed when the Portal is already up and running.
""" """
def _retval(success, txt):
return {"result": amp.dumps((success, txt))}
server_connected = any(1 for prtcl in self.factory.broadcasts server_connected = any(1 for prtcl in self.factory.broadcasts
if prtcl is not self and prtcl.transport.connected) if prtcl is not self and prtcl.transport.connected)
server_pid = self.factory.portal.server_process_id
print("AMP SERVER operation == %s received" % (ord(operation))) logger.log_msg("AMP SERVER operation == %s received" % (ord(operation)))
print("AMP SERVER arguments: %s" % (amp.loads(arguments))) logger.log_msg("AMP SERVER arguments: %s" % (amp.loads(arguments)))
return {"result": "Received."}
if operation == amp.SSTART: # portal start if operation == amp.SSTART: # portal start
# first, check if server is already running # first, check if server is already running
if server_connected: if server_connected:
return {"result": "Server already running at PID={}"} return _retval(False,
"Server already running at PID={spid}".format(spid=server_pid))
else: else:
self.start_server(amp.loads(arguments)) spid = self.start_server(amp.loads(arguments))
return {"result": "Server started with PID {}.".format(0)} # TODO return _retval(True, "Server started with PID {spid}.".format(spid=spid))
elif operation == amp.SRELOAD: # reload server elif operation == amp.SRELOAD: # reload server
if server_connected: if server_connected:
self.reload_server(amp.loads(arguments)) spid = self.reload_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
else: else:
self.start_server(amp.loads(arguments)) self.start_server(amp.loads(arguments))
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
elif operation == amp.SRESET: # reload server
if server_connected:
spid = self.reload_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
else:
self.start_server(amp.loads(arguments))
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
elif operation == amp.PSHUTD: # portal + server shutdown elif operation == amp.PSHUTD: # portal + server shutdown
if server_connected: if server_connected:
self.stop_server(amp.loads(arguments)) self.stop_server()
return _retval(True, "Server stopped.")
self.factory.portal.shutdown(restart=False) self.factory.portal.shutdown(restart=False)
else: else:
raise Exception("operation %(op)s not recognized." % {'op': operation}) raise Exception("operation %(op)s not recognized." % {'op': operation})
@ -257,6 +279,14 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
elif operation == amp.SRELOAD: # server reload elif operation == amp.SRELOAD: # server reload
self.factory.portal.server_reload(**kwargs) self.factory.portal.server_reload(**kwargs)
elif operation == amp.PSYNC: # portal sync
# Server has (re-)connected and wants the session data from portal
sessdata = self.factory.portal.sessions.get_all_sync_data()
self.send_AdminPortal2Server(amp.DUMMYSESSION,
amp.PSYNC,
sessiondata=sessdata)
self.factory.portal.sessions.at_server_connection()
elif operation == amp.SSYNC: # server_session_sync elif operation == amp.SSYNC: # server_session_sync
# server wants to save session data to the portal, # server wants to save session data to the portal,
# maybe because it's about to shut down. # maybe because it's about to shut down.

View file

@ -59,7 +59,11 @@ SCONN = chr(11) # server portal connection (for bots)
PCONNSYNC = chr(12) # portal post-syncing session PCONNSYNC = chr(12) # portal post-syncing session
PDISCONNALL = chr(13) # portal session discnnect all PDISCONNALL = chr(13) # portal session discnnect all
SRELOAD = chr(14) # server reloading (have portal start a new server) SRELOAD = chr(14) # server reloading (have portal start a new server)
SSTART = chr(15) # server start (portal must already be running anyway)
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
# i18n # i18n
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -439,10 +443,19 @@ class ServerSessionHandler(SessionHandler):
Called by server when reloading. We tell the portal to start a new server instance. Called by server when reloading. We tell the portal to start a new server instance.
""" """
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SRELOAD)
def portal_reset_server(self):
"""
Called by server when reloading. We tell the portal to start a new server instance.
"""
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SRESET)
def portal_shutdown(self): def portal_shutdown(self):
""" """
Called by server when shutting down the portal (usually because server is going down too). Called by server when it's time to shut down (the portal will shut us down and then shut
itself down)
""" """
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
@ -572,8 +585,6 @@ class ServerSessionHandler(SessionHandler):
sessiondata=session_data, sessiondata=session_data,
clean=False) clean=False)
def disconnect_all_sessions(self, reason="You have been disconnected."): def disconnect_all_sessions(self, reason="You have been disconnected."):
""" """
Cleanly disconnect all of the connected sessions. Cleanly disconnect all of the connected sessions.