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

View file

@ -13,7 +13,7 @@ from src.server.sessionhandler import SESSIONS
from src.scripts.models import ScriptDB from src.scripts.models import ScriptDB
from src.objects.models import ObjectDB from src.objects.models import ObjectDB
from src.players.models import PlayerDB 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 from src.commands.default.muxcommand import MuxCommand
# limit symbol import for API # limit symbol import for API
@ -213,9 +213,10 @@ class CmdScripts(MuxCommand):
Operate on scripts. Operate on scripts.
Usage: Usage:
@scripts[/switches] [<obj or scriptid>] @scripts[/switches] [<obj or scriptid or script.path>]
Switches: Switches:
start - start a script (must supply a script path)
stop - stops an existing script stop - stops an existing script
kill - kills a script - without running its cleanup hooks kill - kills a script - without running its cleanup hooks
validate - run a validation on the script(s) 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 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 or dbref. For using the /stop switch, a unique script dbref is
required since whole classes of scripts often have the same name. required since whole classes of scripts often have the same name.
Use @script for managing commands on objects.
""" """
key = "@scripts" key = "@scripts"
aliases = "@listscripts" aliases = ["@globalscript", "@listscripts"]
locks = "cmd:perm(listscripts) or perm(Wizards)" locks = "cmd:perm(listscripts) or perm(Wizards)"
help_category = "System" help_category = "System"
@ -239,6 +242,14 @@ class CmdScripts(MuxCommand):
string = "" string = ""
if args: 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 # test first if this is a script match
scripts = ScriptDB.objects.get_all_scripts(key=args) 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. It also defines a few common scripts.
""" """
from sys import getsizeof
from time import time from time import time
from collections import defaultdict
from twisted.internet.defer import maybeDeferred from twisted.internet.defer import maybeDeferred
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet import task from django.conf import settings
from src.server.sessionhandler import SESSIONS from src.server.sessionhandler import SESSIONS
from src.typeclasses.typeclass import TypeClass from src.typeclasses.typeclass import TypeClass
from src.typeclasses.models import _ATTRIBUTE_CACHE
from src.scripts.models import ScriptDB from src.scripts.models import ScriptDB
from src.comms import channelhandler from src.comms import channelhandler
from src.utils import logger 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. # Base script, inherit from Script below instead.
@ -142,7 +147,7 @@ class ScriptClass(TypeClass):
if obj: if obj:
# check so the scripted object is valid and initalized # check so the scripted object is valid and initalized
try: try:
dummy = object.__getattribute__(obj, 'cmdset') object.__getattribute__(obj, 'cmdset')
except AttributeError: except AttributeError:
# this means the object is not initialized. # this means the object is not initialized.
self.dbobj.is_active = False self.dbobj.is_active = False
@ -182,7 +187,7 @@ class ScriptClass(TypeClass):
if self.dbobj.db_interval > 0: if self.dbobj.db_interval > 0:
try: try:
self._stop_task() self._stop_task()
except Exception, e: except Exception:
logger.log_trace("Stopping script %s(%s)" % (self.key, self.dbid)) logger.log_trace("Stopping script %s(%s)" % (self.key, self.dbid))
pass pass
try: try:
@ -217,7 +222,7 @@ class ScriptClass(TypeClass):
self.ndb._paused_time = dt self.ndb._paused_time = dt
self._start_task(start_now=False) self._start_task(start_now=False)
del self.db._paused_time del self.db._paused_time
except Exception, e: except Exception:
logger.log_trace() logger.log_trace()
self.dbobj.is_active = False self.dbobj.is_active = False
return False return False
@ -387,7 +392,7 @@ class DoNothing(Script):
def at_script_creation(self): def at_script_creation(self):
"Setup the script" "Setup the script"
self.key = "sys_do_nothing" self.key = "sys_do_nothing"
self.desc = "This is a placeholder script." self.desc = "This is an empty placeholder script."
class CheckSessions(Script): class CheckSessions(Script):
"Check sessions regularly." "Check sessions regularly."
@ -420,7 +425,6 @@ class ValidateScripts(Script):
class ValidateChannelHandler(Script): class ValidateChannelHandler(Script):
"Update the channelhandler to make sure it's in sync." "Update the channelhandler to make sure it's in sync."
def at_script_creation(self): def at_script_creation(self):
"Setup the script" "Setup the script"
self.key = "sys_channels_validate" self.key = "sys_channels_validate"
@ -433,44 +437,16 @@ class ValidateChannelHandler(Script):
#print "ValidateChannelHandler run." #print "ValidateChannelHandler run."
channelhandler.CHANNELHANDLER.update() channelhandler.CHANNELHANDLER.update()
class AddCmdSet(Script): class ClearAttributeCache(Script):
""" "Clear the attribute cache."
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.
"""
def at_script_creation(self): def at_script_creation(self):
"Setup the script" "Setup the script"
if not self.key: self.key = "sys_cache_clear"
self.key = "add_cmdset" self.desc = "Clears the Attribute Cache"
if not self.desc: self.interval = 3600 * 2
self.desc = "Adds a cmdset to an object."
self.persistent = True self.persistent = True
def at_repeat(self):
# this needs to be assigned to upon creation. "called every 2 hours. Sets a max attr-cache limit to 100 MB." # enough for normal usage?
# It should be a string pointing to the right global _ATTRIBUTE_CACHE
# cmdset module and cmdset class name, e.g. if getsizeof(_ATTRIBUTE_CACHE) / 1024.0 > _ATTRIBUTE_CACHE_MAXSIZE:
# 'examples.cmdset_redbutton.RedButtonCmdSet' _ATTRIBUTE_CACHE = defaultdict(dict)
# 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)

View file

@ -135,7 +135,9 @@ def create_system_scripts():
script2 = create.create_script(scripts.ValidateScripts) script2 = create.create_script(scripts.ValidateScripts)
# update the channel handler to make sure it's in sync # update the channel handler to make sure it's in sync
script3 = create.create_script(scripts.ValidateChannelHandler) 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.") print _(" Error creating system scripts.")
def start_game_time(): def start_game_time():

View file

@ -99,10 +99,15 @@ IDLE_COMMAND = "idle"
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"] ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
# The game server opens an AMP port so that the portal can # The game server opens an AMP port so that the portal can
# communicate with it. This is an internal functionality of Evennia, usually # communicate with it. This is an internal functionality of Evennia, usually
# operating between the two processes on the same machine. Don't change unless # operating between two processes on the same machine. You usually don't need to
# you know what you are doing. # change this unless you cannot use the default AMP port/host for whatever reason.
AMP_HOST = 'localhost' AMP_HOST = 'localhost'
AMP_PORT = 5000 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 # Evennia Database config

View file

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

View file

@ -143,12 +143,34 @@ def c_moves(client):
# otherwise the system will normalize them. # otherwise the system will normalize them.
# #
# 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, ACTIONS = ( c_login,
c_logout, c_logout,
(0.2, c_looks), (0.5, c_looks),
(0.1, c_examines), (0.08, c_examines),
(0.2, c_help), (0.1, c_help),
(0.1, c_digs), (0.01, c_digs),
(0.1, c_creates_obj), (0.01, c_creates_obj),
#(0.1, c_creates_button), #(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 from inspect import ismodule
import os, sys, imp, types, math import os, sys, imp, types, math
from collections import Counter
import textwrap import textwrap
import datetime import datetime
import random 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 Use this function with restrain and only for features/commands
that you know has no influence on the cause-and-effect order of your 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 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 async_func() - function that should be run asynchroneously
at_return(r) - if given, this function will be called when async_func returns at_return(r) - if given, this function will be called when async_func returns