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
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
all objects run scripts that are still valid in the context
@ -153,7 +153,7 @@ class ScriptDBManager(TypedObjectManager):
particular id.
init_mode (str, optional): This is used during server
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
- `"reload"` - server reload. Keep non-persistent scripts.
Returns:

View file

@ -71,11 +71,16 @@ class MonitorHandler(object):
restored_monitors = dbunserialize(restored_monitors)
for (obj, fieldname, idstring, path, persistent, kwargs) in restored_monitors:
try:
if not persistent and not server_reload:
if not server_reload and not persistent:
# this monitor will not be restarted
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)
callback = variable_from_module(modname, varname)
if obj and hasattr(obj, fieldname):
self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs)
except Exception:
@ -116,6 +121,13 @@ class MonitorHandler(object):
persistent (bool, optional): If False, the monitor will survive
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):
# an Attribute - we track its db_value field

View file

@ -355,7 +355,10 @@ def _on_monitor_change(**kwargs):
obj = kwargs["obj"]
name = kwargs["name"]
session = kwargs["session"]
session.msg(monitor={"name": name, "value": _GA(obj, fieldname)})
# 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)})
def monitor(session, *args, **kwargs):

View file

@ -278,17 +278,10 @@ class Evennia(object):
[o.at_init() for o in ObjectDB.get_all_cached_instances()]
[p.at_init() for p in PlayerDB.get_all_cached_instances()]
with open(SERVER_RESTART, 'r') as f:
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'))
mode = self.getset_restart_mode()
# 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
self.at_server_reload_start()
elif mode == 'reset':
@ -301,15 +294,19 @@ class Evennia(object):
# always call this regardless of start type
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
reloading, resetting or shutting down. Valid modes are
'reload', 'reset', 'shutdown' and None.
If mode is None, no change will be done to the flag file.
reloading, resetting or shutting down.
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:
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.
defer.returnValue(None)
mode = self.set_restart_mode(mode)
mode = self.getset_restart_mode(mode)
from evennia.objects.models import ObjectDB
#from evennia.players.models import PlayerDB
@ -423,6 +420,26 @@ class Evennia(object):
if SERVER_STARTSTOP_MODULE:
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):
"""

View file

@ -327,14 +327,12 @@ class ServerSessionHandler(SessionHandler):
self[sessid] = sess
sess.at_sync()
# after sync is complete we force-validate all scripts
# (this also starts them)
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)
# tell the server hook we synced
self.server.at_post_portal_sync()
# announce the reconnection
self.announce_all(_(" ... Server restarted."))
def portal_disconnect(self, session):
"""
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).
Args:
item (packed_session): The fact that item is a packed Session
should be checked before this call.
item (Session)): This item must have all properties of a session
before entering this call.
Returns:
unpacked (any): Either the original input or converts the
internal store back to a Session. If the Session no longer
exists, None is returned.
packed (tuple or None): A session-packed tuple on the form
`(__packed_session__, sessid, conn_time)`. If this sessid
does not match a session in the Session handler, None is returned.
"""
_init_globals()
return item.conn_time and item.sessid and ('__packed_session__',
_GA(item, "sessid"),
_GA(item, "conn_time"))
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__',
_GA(item, "sessid"),
_GA(item, "conn_time"))
return None
def unpack_session(item):
"""
@ -454,7 +460,7 @@ def to_pickle(data):
return item.__class__([process_item(val) for val in item])
except (AttributeError, TypeError):
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_dbobj(item)
return process_item(data)