Added a timeout to the attribute caching; the system will now clean cache at regular intervals once it pass a certain size defined in settings.

This commit is contained in:
Griatch 2012-04-28 00:37:36 +02:00
parent e3ce0a7933
commit 3091587e33
8 changed files with 137 additions and 106 deletions

View file

@ -141,8 +141,10 @@ class Command(object):
input can be either a cmd object or the name of a command.
"""
try:
# first assume input is a command (the most common case)
return cmd.key in self._matchset
except AttributeError: # got a string
except AttributeError:
# probably got a string
return cmd in self._matchset
def __contains__(self, query):
@ -154,7 +156,7 @@ class Command(object):
query (str) - query to match against. Should be lower case.
"""
return any(query in keyalias for keyalias in self._matchset)
return any(query in keyalias for keyalias in self._keyaliases)
def match(self, cmdname):
"""

View file

@ -13,7 +13,7 @@ from src.server.sessionhandler import SESSIONS
from src.scripts.models import ScriptDB
from src.objects.models import ObjectDB
from src.players.models import PlayerDB
from src.utils import logger, utils, gametime
from src.utils import logger, utils, gametime, create
from src.commands.default.muxcommand import MuxCommand
# limit symbol import for API
@ -213,9 +213,10 @@ class CmdScripts(MuxCommand):
Operate on scripts.
Usage:
@scripts[/switches] [<obj or scriptid>]
@scripts[/switches] [<obj or scriptid or script.path>]
Switches:
start - start a script (must supply a script path)
stop - stops an existing script
kill - kills a script - without running its cleanup hooks
validate - run a validation on the script(s)
@ -225,9 +226,11 @@ class CmdScripts(MuxCommand):
will be searched for all scripts defined on it, or an script name
or dbref. For using the /stop switch, a unique script dbref is
required since whole classes of scripts often have the same name.
Use @script for managing commands on objects.
"""
key = "@scripts"
aliases = "@listscripts"
aliases = ["@globalscript", "@listscripts"]
locks = "cmd:perm(listscripts) or perm(Wizards)"
help_category = "System"
@ -239,6 +242,14 @@ class CmdScripts(MuxCommand):
string = ""
if args:
if "start" in self.switches:
# global script-start mode
new_script = create.create_script(args)
if new_script:
caller.msg("Global script %s was started successfully." % args)
else:
caller.msg("Global script %s could not start correctly. See logs." % args)
return
# test first if this is a script match
scripts = ScriptDB.objects.get_all_scripts(key=args)

View file

@ -5,17 +5,22 @@ scripts are inheriting from.
It also defines a few common scripts.
"""
from sys import getsizeof
from time import time
from collections import defaultdict
from twisted.internet.defer import maybeDeferred
from twisted.internet.task import LoopingCall
from twisted.internet import task
from django.conf import settings
from src.server.sessionhandler import SESSIONS
from src.typeclasses.typeclass import TypeClass
from src.typeclasses.models import _ATTRIBUTE_CACHE
from src.scripts.models import ScriptDB
from src.comms import channelhandler
from src.utils import logger
__all__ = ("Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler", "AddCmdSet")
__all__ = ("Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler", "ClearAttributeCache")
_ATTRIBUTE_CACHE_MAXSIZE = settings.ATTRIBUTE_CACHE_MAXSIZE # attr-cache size in MB.
#
# Base script, inherit from Script below instead.
@ -142,7 +147,7 @@ class ScriptClass(TypeClass):
if obj:
# check so the scripted object is valid and initalized
try:
dummy = object.__getattribute__(obj, 'cmdset')
object.__getattribute__(obj, 'cmdset')
except AttributeError:
# this means the object is not initialized.
self.dbobj.is_active = False
@ -182,7 +187,7 @@ class ScriptClass(TypeClass):
if self.dbobj.db_interval > 0:
try:
self._stop_task()
except Exception, e:
except Exception:
logger.log_trace("Stopping script %s(%s)" % (self.key, self.dbid))
pass
try:
@ -217,7 +222,7 @@ class ScriptClass(TypeClass):
self.ndb._paused_time = dt
self._start_task(start_now=False)
del self.db._paused_time
except Exception, e:
except Exception:
logger.log_trace()
self.dbobj.is_active = False
return False
@ -387,7 +392,7 @@ class DoNothing(Script):
def at_script_creation(self):
"Setup the script"
self.key = "sys_do_nothing"
self.desc = "This is a placeholder script."
self.desc = "This is an empty placeholder script."
class CheckSessions(Script):
"Check sessions regularly."
@ -420,7 +425,6 @@ class ValidateScripts(Script):
class ValidateChannelHandler(Script):
"Update the channelhandler to make sure it's in sync."
def at_script_creation(self):
"Setup the script"
self.key = "sys_channels_validate"
@ -433,44 +437,16 @@ class ValidateChannelHandler(Script):
#print "ValidateChannelHandler run."
channelhandler.CHANNELHANDLER.update()
class AddCmdSet(Script):
"""
This script permanently assigns a command set
to an object whenever it is started. This is not
used by the core system anymore, it's here mostly
as an example.
"""
class ClearAttributeCache(Script):
"Clear the attribute cache."
def at_script_creation(self):
"Setup the script"
if not self.key:
self.key = "add_cmdset"
if not self.desc:
self.desc = "Adds a cmdset to an object."
self.key = "sys_cache_clear"
self.desc = "Clears the Attribute Cache"
self.interval = 3600 * 2
self.persistent = True
# this needs to be assigned to upon creation.
# It should be a string pointing to the right
# cmdset module and cmdset class name, e.g.
# 'examples.cmdset_redbutton.RedButtonCmdSet'
# self.db.cmdset = <cmdset_path>
# self.db.add_default = <bool>
def at_start(self):
"Get cmdset and assign it."
cmdset = self.db.cmdset
if cmdset:
if self.db.add_default:
self.obj.cmdset.add_default(cmdset)
else:
self.obj.cmdset.add(cmdset)
def at_stop(self):
"""
This removes the cmdset when the script stops
"""
cmdset = self.db.cmdset
if cmdset:
if self.db.add_default:
self.obj.cmdset.delete_default()
else:
self.obj.cmdset.delete(cmdset)
def at_repeat(self):
"called every 2 hours. Sets a max attr-cache limit to 100 MB." # enough for normal usage?
global _ATTRIBUTE_CACHE
if getsizeof(_ATTRIBUTE_CACHE) / 1024.0 > _ATTRIBUTE_CACHE_MAXSIZE:
_ATTRIBUTE_CACHE = defaultdict(dict)

View file

@ -135,7 +135,9 @@ def create_system_scripts():
script2 = create.create_script(scripts.ValidateScripts)
# update the channel handler to make sure it's in sync
script3 = create.create_script(scripts.ValidateChannelHandler)
if not script1 or not script2 or not script3:
# clear the attribute cache regularly
script4 = create.create_script(scripts.ClearAttributeCache)
if not script1 or not script2 or not script3 or not script4:
print _(" Error creating system scripts.")
def start_game_time():

View file

@ -99,10 +99,15 @@ IDLE_COMMAND = "idle"
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
# The game server opens an AMP port so that the portal can
# communicate with it. This is an internal functionality of Evennia, usually
# operating between the two processes on the same machine. Don't change unless
# you know what you are doing.
# operating between two processes on the same machine. You usually don't need to
# change this unless you cannot use the default AMP port/host for whatever reason.
AMP_HOST = 'localhost'
AMP_PORT = 5000
# Attributes on objects are cached aggressively for speed. If the number of
# objects is large (and their attributes are often accessed) this can use up a lot of
# memory. So every now and then Evennia checks the size of this cache and resets
# it if it's too big. This variable sets the maximum size (in MB).
ATTRIBUTE_CACHE_MAXSIZE = 100
######################################################################
# Evennia Database config

View file

@ -32,6 +32,8 @@ try:
except ImportError:
import pickle
import traceback
from collections import defaultdict
from django.db import models
from django.conf import settings
from django.utils.encoding import smart_str
@ -77,6 +79,11 @@ def _del_cache(obj, name):
except AttributeError:
pass
# this cache holds the attributes loaded on objects, one dictionary
# of attributes per object.
_ATTRIBUTE_CACHE = defaultdict(dict)
#------------------------------------------------------------
#
# Attributes
@ -673,6 +680,7 @@ class TypeNickHandler(object):
#
#------------------------------------------------------------
class TypedObject(SharedMemoryModel):
"""
Abstract Django model.
@ -1213,18 +1221,17 @@ class TypedObject(SharedMemoryModel):
# Helper methods for persistent attributes
_attribute_cache = {}
def has_attribute(self, attribute_name):
"""
See if we have an attribute set on the object.
attribute_name: (str) The attribute's name.
"""
if attribute_name not in _GA(self, "_attribute_cache"):
if attribute_name not in _ATTRIBUTE_CACHE[self]:
attrib_obj = _GA(self, "_attribute_class").objects.filter(db_obj=self).filter(
db_key__iexact=attribute_name)
if attrib_obj:
_GA(self, "_attribute_cache")[attribute_name] = attrib_obj[0]
_ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj[0]
else:
return False
return True
@ -1238,7 +1245,7 @@ class TypedObject(SharedMemoryModel):
new_value: (python obj) The value to set the attribute to. If this is not
a str, the object will be stored as a pickle.
"""
attrib_obj = _GA(self, "_attribute_cache").get("attribute_name")
attrib_obj = _ATTRIBUTE_CACHE[self].get("attribute_name")
if not attrib_obj:
attrclass = _GA(self, "_attribute_class")
# check if attribute already exists.
@ -1252,7 +1259,7 @@ class TypedObject(SharedMemoryModel):
attrib_obj = attrclass(db_key=attribute_name, db_obj=self)
# re-set an old attribute value
attrib_obj.value = new_value
_GA(self,"_attribute_cache")[attribute_name] = attrib_obj
_ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj
def get_attribute(self, attribute_name, default=None):
"""
@ -1263,14 +1270,14 @@ class TypedObject(SharedMemoryModel):
attribute_name: (str) The attribute's name.
default: What to return if no attribute is found
"""
attrib_obj = _GA(self,"_attribute_cache").get(attribute_name)
attrib_obj = _ATTRIBUTE_CACHE[self].get(attribute_name)
if not attrib_obj:
attrib_obj = _GA(self, "_attribute_class").objects.filter(
db_obj=self).filter(db_key__iexact=attribute_name)
if not attrib_obj:
return default
_GA(self,"_attribute_cache")[attribute_name] = attrib_obj[0] #query is first evaluated here
return _GA(self, "_attribute_cache")[attribute_name].value
_ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj[0] #query is first evaluated here
return _ATTRIBUTE_CACHE[self][attribute_name].value
return attrib_obj.value
def get_attribute_raise(self, attribute_name):
@ -1280,14 +1287,14 @@ class TypedObject(SharedMemoryModel):
attribute_name: (str) The attribute's name.
"""
attrib_obj = _GA(self, "_attribute_cache.get")(attribute_name)
attrib_obj = _ATTRIBUTE_CACHE[self].get(attribute_name)
if not attrib_obj:
attrib_obj = _GA(self, "_attribute_class").objects.filter(
db_obj=self).filter(db_key__iexact=attribute_name)
if not attrib_obj:
raise AttributeError
_GA(self, "_attribute_cache")[attribute_name] = attrib_obj[0] #query is first evaluated here
return _GA(self, "_attribute_cache")[attribute_name].value
_ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj[0] #query is first evaluated here
return _ATTRIBUTE_CACHE[self][attribute_name].value
return attrib_obj.value
def del_attribute(self, attribute_name):
@ -1296,9 +1303,9 @@ class TypedObject(SharedMemoryModel):
attribute_name: (str) The attribute's name.
"""
attr_obj = _GA(self, "_attribute_cache").get(attribute_name)
attr_obj = _ATTRIBUTE_CACHE[self].get(attribute_name)
if attr_obj:
del _GA(self, "_attribute_cache")[attribute_name]
del _ATTRIBUTE_CACHE[self][attribute_name]
attr_obj.delete()
else:
try:
@ -1314,9 +1321,9 @@ class TypedObject(SharedMemoryModel):
attribute_name: (str) The attribute's name.
"""
attr_obj = _GA(self, "_attribute_cache").get(attribute_name)
attr_obj = _ATTRIBUTE_CACHE[self].get(attribute_name)
if attr_obj:
del _GA(self, "_attribute_cache")[attribute_name]
del _ATTRIBUTE_CACHE[self][attribute_name]
attr_obj.delete()
else:
try:

View file

@ -1,17 +1,17 @@
"""
These are actions for the dummy client runner, using
the default command set and intended for unmodified Evennia.
These are actions for the dummy client runner, using
the default command set and intended for unmodified Evennia.
Each client action is defined as a function. The clients
will perform these actions randomly (except the login action).
will perform these actions randomly (except the login action).
Each action-definition function should take one argument- "client",
which is a reference to the client currently performing the action
Use the client object for saving data between actions.
Use the client object for saving data between actions.
The client object has the following relevant properties and methods:
cid - unique client id
istep - the current step
cid - unique client id
istep - the current step
exits - an empty list. Can be used to store exit names
objs - an empty list. Can be used to store object names
counter() - get an integer value. This counts up for every call and
@ -19,7 +19,7 @@ The client object has the following relevant properties and methods:
The action-definition function should return the command that the
client should send to the server (as if it was input in a mud client).
It should also return a string detailing the action taken. This string is
It should also return a string detailing the action taken. This string is
used by the "brief verbose" mode of the runner and is prepended by
"Client N " to produce output like "Client 3 is creating objects ..."
@ -30,10 +30,10 @@ are 2-tuples (probability, action_func), where probability defines how
common it is for that particular action to happen. The runner will
randomly pick between those functions based on the probability.
ACTIONS = (login_func, (0.3, func1), (0.1, func2) ... )
ACTIONS = (login_func, (0.3, func1), (0.1, func2) ... )
To change the runner to use your custom ACTION and/or action
definitions, edit settings.py and add
definitions, edit settings.py and add
DUMMYRUNNER_ACTIONS_MODULE = "path.to.your.module"
@ -45,27 +45,27 @@ definitions, edit settings.py and add
import time
RUNID = time.time()
# some convenient templates
# some convenient templates
START_ROOM = "testing_room_start-%s-%s" % (RUNID, "%i")
START_ROOM = "testing_room_start-%s-%s" % (RUNID, "%i")
ROOM_TEMPLATE = "testing_room_%s-%s" % (RUNID, "%i")
EXIT_TEMPLATE = "exit_%s-%s" % (RUNID, "%i")
OBJ_TEMPLATE = "testing_obj_%s-%s" % (RUNID, "%i")
TOBJ_TEMPLATE = "testing_button_%s-%s" % (RUNID, "%i")
TOBJ_TYPECLASS = "examples.red_button.RedButton"
# action function definitions
# action function definitions
def c_login(client):
"logins to the game"
"logins to the game"
cname = "Dummy-%s-%i" % (RUNID, client.cid)
cemail = "%s@dummy.com" % (cname.lower())
cpwd = "%s-%s" % (RUNID, client.cid)
cmd = ('create "%s" %s %s' % (cname, cemail, cpwd),
cmd = ('create "%s" %s %s' % (cname, cemail, cpwd),
'connect %s %s' % (cemail, cpwd),
'@dig %s' % START_ROOM % client.cid,
'@teleport %s' % START_ROOM % client.cid)
return cmd, "logs in as %s ..." % cname
def c_logout(client):
@ -75,18 +75,18 @@ def c_logout(client):
def c_looks(client):
"looks at various objects"
cmd = ["look %s" % obj for obj in client.objs]
if not cmd:
if not cmd:
cmd = ["look %s" % exi for exi in client.exits]
if not cmd:
if not cmd:
cmd = "look"
return cmd, "looks ..."
def c_examines(client):
"examines various objects"
cmd = ["examine %s" % obj for obj in client.objs]
if not cmd:
if not cmd:
cmd = ["examine %s" % exi for exi in client.exits]
if not cmd:
if not cmd:
cmd = "examine me"
return cmd, "examines objs ..."
@ -96,30 +96,30 @@ def c_help(client):
'help @teleport',
'help look',
'help @tunnel',
'help @dig')
'help @dig')
return cmd, "reads help ..."
def c_digs(client):
"digs a new room, storing exit names on client"
"digs a new room, storing exit names on client"
roomname = ROOM_TEMPLATE % client.counter()
exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2])
client.exits.extend([exitname1, exitname2])
cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2)
return cmd, "digs ..."
def c_creates_obj(client):
"creates normal objects, storing their name on client"
"creates normal objects, storing their name on client"
objname = OBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmd = ('@create %s' % objname,
'@desc %s = "this is a test object' % objname,
'@set %s/testattr = this is a test attribute value.' % objname,
'@set %s/testattr2 = this is a second test attribute.' % objname)
'@set %s/testattr2 = this is a second test attribute.' % objname)
return cmd, "creates obj ..."
def c_creates_button(client):
"creates example button, storing name on client"
"creates example button, storing name on client"
objname = TOBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmd = ('@create %s:%s' % (objname, TOBJ_TYPECLASS),
@ -134,21 +134,43 @@ def c_moves(client):
# Action tuple (required)
#
#
# This is a tuple of client action functions. The first element is the
# function the client should use to log into the game and move to
# STARTROOM . The second element is the logout command, for cleanly
# STARTROOM . The second element is the logout command, for cleanly
# exiting the mud. The following elements are 2-tuples of (probability,
# action_function). The probablities should normally sum up to 1,
# otherwise the system will normalize them.
# otherwise the system will normalize them.
#
ACTIONS = ( c_login,
c_logout,
(0.2, c_looks),
(0.1, c_examines),
(0.2, c_help),
(0.1, c_digs),
(0.1, c_creates_obj),
# heavy builder definition
#ACTIONS = ( c_login,
# c_logout,
# (0.2, c_looks),
# (0.1, c_examines),
# (0.2, c_help),
# (0.1, c_digs),
# (0.1, c_creates_obj),
# #(0.1, c_creates_button),
# (0.2, c_moves))
# "normal builder" definition
ACTIONS = ( c_login,
c_logout,
(0.5, c_looks),
(0.08, c_examines),
(0.1, c_help),
(0.01, c_digs),
(0.01, c_creates_obj),
#(0.1, c_creates_button),
(0.2, c_moves))
(0.3, c_moves))
# "normal player" definition
#ACTIONS = ( c_login,
# c_logout,
# (0.4, c_looks),
# #(0.1, c_examines),
# (0.2, c_help),
# #(0.1, c_digs),
# #(0.1, c_creates_obj),
# #(0.1, c_creates_button),
# (0.4, c_moves))

View file

@ -8,7 +8,6 @@ be of use when designing your own game.
"""
from inspect import ismodule
import os, sys, imp, types, math
from collections import Counter
import textwrap
import datetime
import random
@ -460,7 +459,14 @@ def run_async(async_func, at_return=None, at_err=None):
Use this function with restrain and only for features/commands
that you know has no influence on the cause-and-effect order of your
game (commands given after the async function might be executed before
it has finished).
it has finished). Accessing the same property from different threads can
lead to unpredicted behaviour if you are not careful (this is called a
"race condition").
Also note that some databases, notably sqlite3, don't support access from
multiple threads simultaneously, so if you do heavy database access from
your async_func under sqlite3 you will probably run very slow or even get
tracebacks.
async_func() - function that should be run asynchroneously
at_return(r) - if given, this function will be called when async_func returns