Made scripts and typeclassed objects remember db_typeclass_path at all times - a temporarily faulty typeclass will no longer mess up things forever after. Refined and optimized the way typeclasses are cached and loaded, minimizing db hits. The default result when trying to create an object or script with a typeclass that is faulty/not found is now to fail. The previous way, to create an entity anyway using defaults was hard to debug and caused confusion. Resolves issue 175.

This commit is contained in:
Griatch 2011-08-06 18:15:04 +00:00
parent 6cb2b8b745
commit ddfd8120bb
10 changed files with 208 additions and 173 deletions

View file

@ -19,21 +19,20 @@ class BodyFunctions(Script):
This class defines the script itself
"""
def at_script_creation(self):
def at_script_creation(self):
self.key = "bodyfunction"
self.desc = "Adds various timed events to a character."
self.interval = 20 # seconds
#self.repeats = 5 # repeat only a certain number of times
self.start_delay = True # wait self.interval until first call
self.persistent = False
self.persistent = True
def at_repeat(self):
"""
This gets called every self.interval seconds. We make
a random check here so as to only return 33% of the time.
"""
#)
if random.random() < 0.33:
if random.random() < 0.66:
# no message this time
return
rand = random.random()

View file

@ -788,6 +788,7 @@ class ObjectDB(TypedObject):
objects to their respective home locations, as well as clean
up all exits to/from the object.
"""
if not self.at_object_delete():
# this is an extra pre-check
# run before deletion mechanism

View file

@ -253,3 +253,15 @@ class ScriptDB(TypedObject):
default_typeclass_path = settings.DEFAULT_SCRIPT_TYPECLASS
except:
default_typeclass_path = "src.scripts.scripts.DoNothing"
def at_typeclass_error(self):
"""
If this is called, it means the typeclass has a critical
error and cannot even be loaded. We don't allow a script
to be created under those circumstances. Already created,
permanent scripts are set to already be active so they
won't get activated now (next reboot the bug might be fixed)
"""
# By setting is_active=True, we trick the script not to run "again".
self.is_active = True
return super(ScriptDB, self).at_typeclass_error()

View file

@ -54,10 +54,10 @@ class ScriptHandler(object):
or a python path to such a class object.
key - optional identifier for the script (often set in script definition)
autostart - start the script upon adding it
"""
"""
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=autostart)
if not script:
logger.log_errmsg("Script %s failed to be created/start." % scriptclass)
logger.log_errmsg("Script %s could not be created and/or started." % scriptclass)
return False
return True

View file

@ -378,11 +378,11 @@ class Script(ScriptClass):
# Some useful default Script types used by Evennia.
class DoNothing(Script):
"An script that does nothing. Used as default."
"An script that does nothing. Used as default fallback."
def at_script_creation(self):
"Setup the script"
self.key = "sys_do_nothing"
self.desc = "This does nothing."
self.desc = "This is a placeholder script."
class CheckSessions(Script):
"Check sessions regularly."

View file

@ -52,14 +52,6 @@ PARENTS = {
"channel":"src.comms.models.Channel",
"helpentry":"src.help.models.HelpEntry"}
# cached typeclasses for all typed models
TYPECLASS_CACHE = {}
def reset():
"Clean out the typeclass cache"
global TYPECLASS_CACHE
TYPECLASS_CACHE = {}
#------------------------------------------------------------
#
# Attributes
@ -426,6 +418,10 @@ class TypedObject(SharedMemoryModel):
# Database manager
objects = managers.TypedObjectManager()
# object cache and flags
cached_typeclass_path = ""
cached_typeclass = None
# lock handler self.locks
def __init__(self, *args, **kwargs):
"We must initialize the parent first - important!"
@ -485,17 +481,22 @@ class TypedObject(SharedMemoryModel):
#@property
def typeclass_path_get(self):
"Getter. Allows for value = self.typeclass_path"
typeclass_path = object.__getattribute__(self, 'cached_typeclass_path')
if typeclass_path:
return typeclass_path
return self.db_typeclass_path
#@typeclass_path.setter
def typeclass_path_set(self, value):
"Setter. Allows for self.typeclass_path = value"
self.db_typeclass_path = value
self.save()
object.__setattr__(self, "cached_typeclass_path", value)
#@typeclass_path.deleter
def typeclass_path_del(self):
"Deleter. Allows for del self.typeclass_path"
self.db_typeclass_path = None
self.db_typeclass_path = ""
self.save()
self.cached_typeclass_path = ""
typeclass_path = property(typeclass_path_get, typeclass_path_set, typeclass_path_del)
# date_created property
@ -613,96 +614,61 @@ class TypedObject(SharedMemoryModel):
it allows for extending the Typed object for all different
types of objects that the game needs. This property
handles loading and initialization of the typeclass on the fly.
"""
def errmsg(message):
"""
Helper function to display error.
"""
infochan = None
cmessage = message
try:
from src.comms.models import Channel
infochan = settings.CHANNEL_MUDINFO
infochan = Channel.objects.get_channel(infochan[0])
if infochan:
cname = infochan.key
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')])
infochan.msg(message)
logger.log_errmsg(cmessage)
else:
# no mudinfo channel is found. Log instead.
cmessage = "\n".join(["[NO MUDINFO CHANNEL]: %s" % line for line in message.split('\n')])
logger.log_errmsg(cmessage)
except Exception, e:
if ServerConfig.objects.conf("server_starting_mode"):
print cmessage
else:
logger.log_trace(cmessage)
path = object.__getattribute__(self, 'db_typeclass_path')
#print "typeclass_loading:", id(self), path
Note: The liberal use of object.__getattribute__ and __setattr__ (instead
of normal dot notation) is due to optimization: it avoids calling
the custom self.__getattribute__ more than necessary.
"""
path = object.__getattribute__(self, "cached_typeclass_path")
if not path:
path = object.__getattribute__(self, 'db_typeclass_path')
typeclass = object.__getattribute__(self, "cached_typeclass")
try:
if typeclass and object.__getattribute__(typeclass, "path") == path:
return typeclass
except AttributeError:
pass
errstring = ""
if not path:
# this means we should get the default obj
# without giving errors.
defpath = object.__getattribute__(self, 'default_typeclass_path')
typeclass = object.__getattribute__(self, '_path_import')(defpath)
#typeclass = self._path_import(defpath)
# this means we should get the default obj without giving errors.
return object.__getattribute__(self, "get_default_typeclass")(cache=True, silent=True, save=True)
else:
# handle loading/importing of typeclasses, searching all paths.
# (self.typeclss_paths is a shortcut to settings.TYPECLASS_*_PATH
# (self.typeclass_paths is a shortcut to settings.TYPECLASS_*_PATHS
# where '*' is either OBJECT, SCRIPT or PLAYER depending on the typed
# object).
typeclass_paths = [path] + ["%s.%s" % (prefix, path) for prefix in self.typeclass_paths]
# entities).
typeclass_paths = [path] + ["%s.%s" % (prefix, path) for prefix in object.__getattribute__(self, 'typeclass_paths')]
for tpath in typeclass_paths:
# try to find any matches to the typeclass path, in all possible permutations..
typeclass = TYPECLASS_CACHE.get(tpath, None)
if typeclass:
# we've imported this before. We're done.
return typeclass
# not in cache. Try to import anew.
# try to import and analyze the result
typeclass = object.__getattribute__(self, "_path_import")(tpath)
if callable(typeclass):
# don't return yet, we must cache this further down.
errstring = ""
break # leave test loop
# we succeeded to import. Cache and return.
object.__setattr__(self, 'db_typeclass_path', tpath)
object.__getattribute__(self, 'save')()
object.__setattr__(self, "cached_typeclass_path", tpath)
object.__setattr__(self, "cached_typeclass", typeclass)
return typeclass
elif hasattr(typeclass, '__file__'):
errstring += "\n%s seems to be just the path to a module. You need" % tpath
errstring += " to specify the actual typeclass name inside the module too."
else:
errstring += "\n%s" % typeclass # this will hold an error message.
if not callable(typeclass):
# Still not a valid import. Fallback to default.
# Note that we don't save to this changed path! Fix the typeclass
# definition instead.
defpath = object.__getattribute__(self, "default_typeclass_path")
errstring += "\n\nUsing Default class '%s'." % defpath
typeclass = object.__getattribute__(self, "_path_import")(defpath)
errmsg(errstring)
if not callable(typeclass):
# if typeclass still doesn't exist at this point, we're in trouble.
# fall back to hardcoded core class which is wrong for e.g. scripts/players etc.
errstring = " %s\n%s" % (typeclass, errstring)
errstring += " Default class '%s' failed to load." % defpath
defpath = "src.objects.objects.Object"
errstring += "\n Using Evennia's default class '%s'." % defpath
typeclass = object.__getattribute__(self, "_path_import")(defpath)
errmsg(errstring)
else:
TYPECLASS_CACHE[path] = typeclass
return typeclass
errstring += "\n%s" % typeclass # this will hold a growing error message.
# If we reach this point we couldn't import any typeclasses. Return default. It's up to the calling
# method to use e.g. self.is_typeclass() to detect that the result is not the one asked for.
object.__getattribute__(self, "_display_errmsg")(errstring)
return object.__getattribute__(self, "get_default_typeclass")(cache=False, silent=False, save=False)
#@typeclass.deleter
def typeclass_del(self):
"Deleter. Allows for del self.typeclass (don't allow deletion)"
raise Exception("The typeclass property should never be deleted!")
"Deleter. Disallow 'del self.typeclass'"
raise Exception("The typeclass property should never be deleted, only changed in-place!")
# typeclass property
typeclass = property(typeclass_get, fdel=typeclass_del)
def _path_import(self, path):
"""
@ -731,38 +697,95 @@ class TypedObject(SharedMemoryModel):
errstring = "No class '%s' was found in module '%s'."
errstring = errstring % (class_name, modpath)
except Exception:
trc = traceback.format_exc()
errstring = "\n%sException importing '%s'." % (trc, path)
trc = traceback.format_exc()
errstring = "\n%sException importing '%s'." % (trc, path)
# return the error.
return errstring
def _display_errmsg(self, message):
"""
Helper function to display error.
"""
infochan = None
cmessage = message
try:
from src.comms.models import Channel
infochan = settings.CHANNEL_MUDINFO
infochan = Channel.objects.get_channel(infochan[0])
if infochan:
cname = infochan.key
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')])
infochan.msg(message)
else:
# no mudinfo channel is found. Log instead.
cmessage = "\n".join(["[NO MUDINFO CHANNEL]: %s" % line for line in message.split('\n')])
logger.log_errmsg(cmessage)
except Exception, e:
if ServerConfig.objects.conf("server_starting_mode"):
print cmessage
else:
logger.log_trace(cmessage)
def is_typeclass(self, other_typeclass, exact=False):
def get_default_typeclass(self, cache=False, silent=False, save=False):
"""
This is called when a typeclass fails to
load for whatever reason.
Overload this in different entities.
Default operation is to load a default typeclass.
"""
defpath = object.__getattribute__(self, "default_typeclass_path")
typeclass = object.__getattribute__(self, "_path_import")(defpath)
# if not silent:
# #errstring = "\n\nUsing Default class '%s'." % defpath
# object.__getattribute__(self, "_display_errmsg")(errstring)
if not callable(typeclass):
# if typeclass still doesn't exist at this point, we're in trouble.
# fall back to hardcoded core class which is wrong for e.g. scripts/players etc.
failpath = defpath
defpath = "src.objects.objects.Object"
typeclass = object.__getattribute__(self, "_path_import")(defpath)
if not silent:
errstring += " %s\n%s" % (typeclass, errstring)
errstring += " Default class '%s' failed to load." % failpath
errstring += "\n Using Evennia's default class '%s'." % defpath
object.__getattribute__(self, "_display_errmsg")(errstring)
if not callable(typeclass):
# if this is still giving an error, Evennia is wrongly configured or buggy
raise Exception("CRITICAL ERROR: The final fallback typeclass %s cannot load!!" % defpath)
if save:
object.__setattr__(self, 'db_typeclass_path', defpath)
object.__getattribute__(self, 'save')()
if cache:
object.__setattr__(self, "cached_typeclass_path", defpath)
object.__setattr__(self, "cached_typeclass", typeclass)
return typeclass
def is_typeclass(self, typeclass, exact=False):
"""
Returns true if this object has this type
OR has a typeclass which is an subclass of
the given typeclass.
other_typeclass - can be a class object or the
python path to such an object.
typeclass - can be a class object or the
python path to such an object to match against.
exact - returns true only if the object's
type is exactly this typeclass, ignoring
parents.
"""
if callable(other_typeclass):
# this is an actual class object. Get the path to it.
cls = other_typeclass.__class__
other_typeclass = "%s.%s" % (cls.__module__, cls.__name__)
if not other_typeclass:
return False
if self.db_typeclass_path == other_typeclass:
return True
if not exact:
# check the parent chain.
"""
try:
typeclass = typeclass.path
except AttributeError:
pass
if exact:
current_path = object.__getattribute__(self, "cached_typeclass_path")
return typeclass and current_path == typeclass
else:
# check parent chain
return any([cls for cls in self.typeclass.mro()
if other_typeclass == "%s.%s" % (cls.__module__,
cls.__name__)])
return False
if "%s.%s" % (cls.__module__, cls.__name__) == typeclass])
#
# Object manipulation methods
@ -826,8 +849,6 @@ class TypedObject(SharedMemoryModel):
new_typeclass.basetype_setup()
new_typeclass.at_object_creation()
#
# Attribute handler methods
#

View file

@ -19,7 +19,7 @@ from django.conf import settings
# to *in-game* safety (if you can edit typeclasses you have
# full access anyway), so no protection against changing
# e.g. 'locks' or 'permissions' should go here.
PROTECTED = ['id', 'dbobj', 'db', 'objects', 'typeclass',
PROTECTED = ['id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass',
'attr', 'save', 'delete']
# If this is true, all non-protected property assignments
@ -70,8 +70,8 @@ class TypeClass(object):
o = dbobj.object_class(dbobj) : this is used when dbobj.object_class is already set.
"""
# typecheck of dbobj - we can't allow it to be added here unless
# unless it's really a TypedObject.
# typecheck of dbobj - we can't allow it to be added here
# unless it's really a TypedObject.
dbobj_cls = object.__getattribute__(dbobj, '__class__')
dbobj_mro = object.__getattribute__(dbobj_cls, '__mro__')
if not any('src.typeclasses.models.TypedObject'
@ -83,16 +83,13 @@ class TypeClass(object):
# store the needed things on the typeclass
object.__setattr__(self, '_protected_attrs', PROTECTED)
# sync the database object to this typeclass.
cls = object.__getattribute__(self, '__class__')
db_typeclass_path = "%s.%s" % (object.__getattribute__(cls, '__module__'),
object.__getattribute__(cls, '__name__'))
if not object.__getattribute__(dbobj, "db_typeclass_path") == db_typeclass_path:
object.__setattr__(dbobj, "db_typeclass_path", db_typeclass_path)
object.__getattribute__(dbobj, "save")()
# (The inheriting typed object classes often extend this __init__ to
# add handlers etc.)
# # sync the database object to this typeclass.
# cls = object.__getattribute__(self, '__class__')
# db_typeclass_path = "%s.%s" % (object.__getattribute__(cls, '__module__'),
# object.__getattribute__(cls, '__name__'))
# if not object.__getattribute__(dbobj, "db_typeclass_path") == db_typeclass_path:
# object.__setattr__(dbobj, "db_typeclass_path", db_typeclass_path)
# object.__getattribute__(dbobj, "save")()
def __getattribute__(self, propname):
"""

View file

@ -21,8 +21,9 @@ Models covered:
from django.conf import settings
from django.contrib.auth.models import User
from django.db import IntegrityError
from src.utils import logger, utils
from src.utils.utils import is_iter, has_parent
from src.utils.idmapper.models import SharedMemoryModel
from src.utils import logger, utils, idmapper
from src.utils.utils import is_iter, has_parent, inherits_from
#
# Game Object creation
@ -45,30 +46,28 @@ def create_object(typeclass, key=None, location=None,
# deferred import to avoid loops
from src.objects.objects import Object
from src.objects.models import ObjectDB
#print "in create_object", typeclass
if isinstance(typeclass, ObjectDB):
# this is already an objectdb instance!
new_db_object = typeclass
typeclass = new_db_object.typeclass
elif isinstance(typeclass, Object):
# this is already an object typeclass!
new_db_object = typeclass.dbobj
typeclass = typeclass.__class__
else:
# create database object
new_db_object = ObjectDB()
#new_db_object = ObjectDB()
if not callable(typeclass):
# this means typeclass might be a path. If not,
# the type mechanism will automatically assign
# the BASE_OBJECT_TYPE from settings.
if typeclass:
typeclass = utils.to_unicode(typeclass)
new_db_object.typeclass_path = typeclass
new_db_object.save()
# this will either load the typeclass or the default one
typeclass = new_db_object.typeclass
new_db_object.save()
# this is already an objectdb instance, extract its typeclass
typeclass = new_db_object.typeclass.path
elif isinstance(typeclass, Object) or utils.inherits_from(typeclass, Object):
# this is already an object typeclass, extract its path
typeclass = typeclass.path
# create new database object
new_db_object = ObjectDB()
# assign the typeclass
typeclass = utils.to_unicode(typeclass)
new_db_object.typeclass_path = typeclass
# this will either load the typeclass or the default one
typeclass = new_db_object.typeclass
if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True):
# this will fail if we gave a typeclass as input and it still gave us a default
SharedMemoryModel.delete(new_db_object)
return None
# the name/key is often set later in the typeclass. This
# is set here as a failsafe.
if key:
@ -142,33 +141,37 @@ def create_script(typeclass, key=None, obj=None, locks=None, autostart=True):
See src.scripts.manager for methods to manipulate existing
scripts in the database.
"""
# deferred import to avoid loops.
from src.scripts.scripts import Script
#print "in create_script", typeclass
from src.scripts.models import ScriptDB
if isinstance(typeclass, ScriptDB):
#print "this is already a scriptdb instance!"
new_db_object = typeclass
typeclass = new_db_object.typeclass
elif isinstance(typeclass, Script):
#print "this is already an object typeclass!", typeclass, typeclass.__class__
new_db_object = typeclass.dbobj
typeclass = typeclass.__class__
else:
# create a new instance.
new_db_object = ScriptDB()
#new_db_object = ScriptDB()
if not callable(typeclass):
# try to load this in case it's a path
if typeclass:
typeclass = utils.to_unicode(typeclass)
new_db_object.db_typeclass_path = typeclass
new_db_object.save()
# this will load either the typeclass or the default one
typeclass = new_db_object.typeclass
new_db_object.save()
# this is already an objectdb instance, extract its typeclass
typeclass = new_db_object.typeclass.path
elif isinstance(typeclass, Script) or utils.inherits_from(typeclass, Script):
# this is already an object typeclass, extract its path
typeclass = typeclass.path
# create new database object
new_db_object = ScriptDB()
# assign the typeclass
typeclass = utils.to_unicode(typeclass)
new_db_object.typeclass_path = typeclass
# this will either load the typeclass or the default one
typeclass = new_db_object.typeclass
if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True):
# this can happen if the default was loaded (due to
# inability to load given typeclass), which we
# don't accept during creation.
SharedMemoryModel.delete(new_db_object)
return None
# the typeclass is initialized
new_script = typeclass(new_db_object)
# store variables on the typeclass (which means
# it's actually transparently stored on the db object)
@ -388,8 +391,8 @@ def create_player(name, email, password,
new_user = User.objects.create_user(name, email, password)
# create the associated Player for this User, and tie them together
new_player = PlayerDB(db_key=name, user=new_user)
new_player.save()
new_player = PlayerDB(db_key=name, user=new_user)
new_player.save()
new_player.basetype_setup() # setup the basic locks and cmdset
# call hook method (may override default permissions)

View file

@ -101,8 +101,11 @@ class SharedMemoryModel(Model):
"""
if instance._get_pk_val() is not None:
cls.__instance_cache__[instance._get_pk_val()] = instance
if hasattr(instance, 'at_cache') and callable(instance.at_cache):
instance.at_cache()
try:
object.__getattribute__(instance, "at_cache")()
except (TypeError, AttributeError):
pass
#key = "%s-%s" % (cls, instance.pk)
#TCACHE[key] = instance
#print "cached: %s (%s: %s) (total cached: %s)" % (instance, cls.__name__, len(cls.__instance_cache__), len(TCACHE))

View file

@ -122,8 +122,7 @@ def reload_modules():
else:
cemit_info(" No modules reloaded.")
# clean out cache dictionary of typeclasses, exits and channels
typeclassmodels.reset()
# clean out cache dictionary of typeclasses, exits and channels
channelhandler.CHANNELHANDLER.update()
# run through all objects in database, forcing re-caching.