Fix error in MonitorHandler recovering a saved Session across a reload. This probably affected the TickerHandler as well. Add a new hook to the server object that gets called once the portal has synced, and run the monitorhandler/tickerhandler restores there. Also some changes to the serialization of Sessions. Resolves #1164.

This commit is contained in:
Griatch 2017-01-15 19:55:51 +01:00
parent b46bc9b2aa
commit 052e1845a2
6 changed files with 70 additions and 34 deletions

View file

@ -133,7 +133,7 @@ class ScriptDBManager(TypedObjectManager):
return nr_deleted return nr_deleted
def validate(self, scripts=None, obj=None, key=None, dbref=None, def validate(self, scripts=None, obj=None, key=None, dbref=None,
init_mode=False): init_mode=None):
""" """
This will step through the script database and make sure This will step through the script database and make sure
all objects run scripts that are still valid in the context all objects run scripts that are still valid in the context
@ -153,7 +153,7 @@ class ScriptDBManager(TypedObjectManager):
particular id. particular id.
init_mode (str, optional): This is used during server init_mode (str, optional): This is used during server
upstart and can have three values: upstart and can have three values:
- `False` (no init mode). Called during run. - `None` (no init mode). Called during run.
- `"reset"` - server reboot. Kill non-persistent scripts - `"reset"` - server reboot. Kill non-persistent scripts
- `"reload"` - server reload. Keep non-persistent scripts. - `"reload"` - server reload. Keep non-persistent scripts.
Returns: Returns:

View file

@ -71,11 +71,16 @@ class MonitorHandler(object):
restored_monitors = dbunserialize(restored_monitors) restored_monitors = dbunserialize(restored_monitors)
for (obj, fieldname, idstring, path, persistent, kwargs) in restored_monitors: for (obj, fieldname, idstring, path, persistent, kwargs) in restored_monitors:
try: try:
if not persistent and not server_reload: if not server_reload and not persistent:
# this monitor will not be restarted # this monitor will not be restarted
continue continue
if "session" in kwargs and not kwargs["session"]:
# the session was removed because it no longer
# exists. Don't restart the monitor.
continue
modname, varname = path.rsplit(".", 1) modname, varname = path.rsplit(".", 1)
callback = variable_from_module(modname, varname) callback = variable_from_module(modname, varname)
if obj and hasattr(obj, fieldname): if obj and hasattr(obj, fieldname):
self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs) self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs)
except Exception: except Exception:
@ -116,6 +121,13 @@ class MonitorHandler(object):
persistent (bool, optional): If False, the monitor will survive persistent (bool, optional): If False, the monitor will survive
a server reload but not a cold restart. This is default. a server reload but not a cold restart. This is default.
Kwargs:
session (Session): If this keyword is given, the monitorhandler will
correctly analyze it and remove the monitor if after a reload/reboot
the session is no longer valid.
any (any): Any other kwargs are passed on to the callback. Remember that
all kwargs must be possible to pickle!
""" """
if not fieldname.startswith("db_") or not hasattr(obj, fieldname): if not fieldname.startswith("db_") or not hasattr(obj, fieldname):
# an Attribute - we track its db_value field # an Attribute - we track its db_value field

View file

@ -355,6 +355,9 @@ def _on_monitor_change(**kwargs):
obj = kwargs["obj"] obj = kwargs["obj"]
name = kwargs["name"] name = kwargs["name"]
session = kwargs["session"] session = kwargs["session"]
# the session may be None if the char quits and someone
# else then edits the object
if session:
session.msg(monitor={"name": name, "value": _GA(obj, fieldname)}) session.msg(monitor={"name": name, "value": _GA(obj, fieldname)})

View file

@ -278,17 +278,10 @@ class Evennia(object):
[o.at_init() for o in ObjectDB.get_all_cached_instances()] [o.at_init() for o in ObjectDB.get_all_cached_instances()]
[p.at_init() for p in PlayerDB.get_all_cached_instances()] [p.at_init() for p in PlayerDB.get_all_cached_instances()]
with open(SERVER_RESTART, 'r') as f: mode = self.getset_restart_mode()
mode = f.read()
if mode in ('True', 'reload'):
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.restore()
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore(mode in ('True', 'reload'))
# call correct server hook based on start file value # call correct server hook based on start file value
if mode in ('True', 'reload'): if mode == 'reload':
# True was the old reload flag, kept for compatibilty # True was the old reload flag, kept for compatibilty
self.at_server_reload_start() self.at_server_reload_start()
elif mode == 'reset': elif mode == 'reset':
@ -301,15 +294,19 @@ class Evennia(object):
# always call this regardless of start type # always call this regardless of start type
self.at_server_start() self.at_server_start()
def set_restart_mode(self, mode=None): def getset_restart_mode(self, mode=None):
""" """
This manages the flag file that tells the runner if the server is This manages the flag file that tells the runner if the server is
reloading, resetting or shutting down. Valid modes are reloading, resetting or shutting down.
'reload', 'reset', 'shutdown' and None.
If mode is None, no change will be done to the flag file. Args:
mode (string or None, optional): Valid values are
'reload', 'reset', 'shutdown' and `None`. If mode is `None`,
no change will be done to the flag file.
Returns:
mode (str): The currently active restart mode, either just
set or previously set.
Either way, the active restart setting (Restart=True/False) is
returned so the server knows which more it's in.
""" """
if mode is None: if mode is None:
with open(SERVER_RESTART, 'r') as f: with open(SERVER_RESTART, 'r') as f:
@ -343,7 +340,7 @@ class Evennia(object):
# once; we don't need to run the shutdown procedure again. # once; we don't need to run the shutdown procedure again.
defer.returnValue(None) defer.returnValue(None)
mode = self.set_restart_mode(mode) mode = self.getset_restart_mode(mode)
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
#from evennia.players.models import PlayerDB #from evennia.players.models import PlayerDB
@ -423,6 +420,26 @@ class Evennia(object):
if SERVER_STARTSTOP_MODULE: if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_reload_start() SERVER_STARTSTOP_MODULE.at_server_reload_start()
def at_post_portal_sync(self):
"""
This is called just after the portal has finished syncing back data to the server
after reconnecting.
"""
# one of reload, reset or shutdown
mode = self.getset_restart_mode()
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.restore(mode == 'reload')
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore(mode == 'reload')
# after sync is complete we force-validate all scripts
# (this also starts any that didn't yet start)
ScriptDB.objects.validate(init_mode=mode)
# delete the temporary setting
ServerConfig.objects.conf("server_restart_mode", delete=True)
def at_server_reload_stop(self): def at_server_reload_stop(self):
""" """

View file

@ -327,14 +327,12 @@ class ServerSessionHandler(SessionHandler):
self[sessid] = sess self[sessid] = sess
sess.at_sync() sess.at_sync()
# after sync is complete we force-validate all scripts # tell the server hook we synced
# (this also starts them) self.server.at_post_portal_sync()
init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None)
_ScriptDB.objects.validate(init_mode=init_mode)
_ServerConfig.objects.conf("server_restart_mode", delete=True)
# announce the reconnection # announce the reconnection
self.announce_all(_(" ... Server restarted.")) self.announce_all(_(" ... Server restarted."))
def portal_disconnect(self, session): def portal_disconnect(self, session):
""" """
Called from Portal when Portal session closed from the portal Called from Portal when Portal session closed from the portal

View file

@ -375,19 +375,25 @@ def pack_session(item):
can't be safely serialized). can't be safely serialized).
Args: Args:
item (packed_session): The fact that item is a packed Session item (Session)): This item must have all properties of a session
should be checked before this call. before entering this call.
Returns: Returns:
unpacked (any): Either the original input or converts the packed (tuple or None): A session-packed tuple on the form
internal store back to a Session. If the Session no longer `(__packed_session__, sessid, conn_time)`. If this sessid
exists, None is returned. does not match a session in the Session handler, None is returned.
""" """
_init_globals() _init_globals()
session = _SESSION_HANDLER.get(item.sessid)
if session and session.conn_time == item.conn_time:
# we require connection times to be identical for the Session
# to be accepted as actually being a session (sessids gets
# reused all the time).
return item.conn_time and item.sessid and ('__packed_session__', return item.conn_time and item.sessid and ('__packed_session__',
_GA(item, "sessid"), _GA(item, "sessid"),
_GA(item, "conn_time")) _GA(item, "conn_time"))
return None
def unpack_session(item): def unpack_session(item):
""" """
@ -454,7 +460,7 @@ def to_pickle(data):
return item.__class__([process_item(val) for val in item]) return item.__class__([process_item(val) for val in item])
except (AttributeError, TypeError): except (AttributeError, TypeError):
return [process_item(val) for val in item] return [process_item(val) for val in item]
elif hasattr(item, "sessid") and hasattr(item, "conn_time") and item.sessid in _SESSION_HANDLER: elif hasattr(item, "sessid") and hasattr(item, "conn_time"):
return pack_session(item) return pack_session(item)
return pack_dbobj(item) return pack_dbobj(item)
return process_item(data) return process_item(data)