From 677a34d06e5a6110afe87dca3c86281947187bd2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 2 Jun 2021 00:24:21 +0200 Subject: [PATCH] Cleaned up dummyrunner and fixed a lot of issues --- evennia/server/portal/portalsessionhandler.py | 3 +- evennia/server/profiling/dummyrunner.py | 264 +++++++++++++++--- .../server/profiling/dummyrunner_settings.py | 250 ++++++++++------- evennia/server/profiling/settings_mixin.py | 23 ++ evennia/server/throttle.py | 7 +- evennia/settings_default.py | 3 +- 6 files changed, 408 insertions(+), 142 deletions(-) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 5053add9c3..b0c5222ca9 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -37,7 +37,8 @@ DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0) # Portal-SessionHandler class # ------------------------------------------------------------- -DOS_PROTECTION_MSG = _("{servername} DoS protection is active. You are queued to connect in {num} seconds ...") +DOS_PROTECTION_MSG = _("{servername} DoS protection is active." + "You are queued to connect in {num} seconds ...") class PortalSessionHandler(SessionHandler): diff --git a/evennia/server/profiling/dummyrunner.py b/evennia/server/profiling/dummyrunner.py index 3b13bb49f6..50f65872fb 100644 --- a/evennia/server/profiling/dummyrunner.py +++ b/evennia/server/profiling/dummyrunner.py @@ -40,8 +40,16 @@ from twisted.conch import telnet from twisted.internet import reactor, protocol from twisted.internet.task import LoopingCall -from django.conf import settings -from evennia.utils import mod_import, time_format +import django +django.setup() +import evennia # noqa +evennia._init() + +from django.conf import settings # noqa +from evennia.utils import mod_import, time_format # noqa +from evennia.commands.command import Command # noqa +from evennia.commands.cmdset import CmdSet # noqa +from evennia.utils.ansi import strip_ansi # noqa # Load the dummyrunner settings module @@ -51,8 +59,10 @@ if not DUMMYRUNNER_SETTINGS: "Error: Dummyrunner could not find settings file at %s" % settings.DUMMYRUNNER_SETTINGS_MODULE ) +IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE DATESTRING = "%Y%m%d%H%M%S" +CLIENTS = [] # Settings @@ -71,18 +81,37 @@ CHANCE_OF_LOGIN = DUMMYRUNNER_SETTINGS.CHANCE_OF_LOGIN # Port to use, if not specified on command line TELNET_PORT = DUMMYRUNNER_SETTINGS.TELNET_PORT or settings.TELNET_PORTS[0] # -NLOGGED_IN = 0 +NCONNECTED = 0 # client has received a connection +NLOGIN_SCREEN = 0 # client has seen the login screen (server responded) +NLOGGING_IN = 0 # client starting login procedure +NLOGGED_IN = 0 # client has authenticated and logged in - -# Messages +# time when all clients have logged_in +TIME_ALL_LOGIN = 0 +# actions since all logged in +TOTAL_ACTIONS = 0 +TOTAL_LAG_MEASURES = 0 +# lag per 30s for all logged in +TOTAL_LAG = 0 +TOTAL_LAG_IN = 0 +TOTAL_LAG_OUT = 0 INFO_STARTING = """ - Dummyrunner starting using {N} dummy account(s). If you don't see + Dummyrunner starting using {nclients} dummy account(s). If you don't see any connection messages, make sure that the Evennia server is running. - Use Ctrl-C to stop/disconnect clients. + TELNET_PORT = {port} + IDMAPPER_CACHE_MAXSIZE = {idmapper_cache_size} MB + TIMESTEP = {timestep} (rate {rate}/s) + CHANCE_OF_LOGIN = {chance_of_login}% per time step + CHANCE_OF_ACTION = {chance_of_action}% per time step + -> avg rate (per client, after login): {avg_rate} cmds/s + -> total avg rate (after login): {avg_rate_total} cmds/s + + Use Ctrl-C (or Cmd-C) to stop/disconnect all clients. + """ ERROR_NO_MIXIN = """ @@ -97,6 +126,7 @@ ERROR_NO_MIXIN = """ to test all commands - change PASSWORD_HASHERS to use a faster (but less safe) algorithm when creating large numbers of accounts at the same time + - set LOGIN_THROTTLE/CREATION_THROTTLE=None to disable it If you don't want to use the custom settings of the mixin for some reason, you can change their values manually after the import, or @@ -168,6 +198,39 @@ until you see the initial login slows things too much. """ + +class CmdDummyRunnerEchoResponse(Command): + """ + Dummyrunner command measuring the round-about response time + from sending to receiving a result. + + Usage: + dummyrunner_echo_response + + Responds with + dummyrunner_echo_response:, + + The dummyrunner will send this and then compare the send time + with the receive time on both ends. + + """ + key = "dummyrunner_echo_response" + + def func(self): + # returns (dummy_client_timestamp,current_time) + self.msg(f"dummyrunner_echo_response:{self.args},{time.time()}") + if self.caller.account.is_superuser: + print(f"cmddummyrunner lag in: {time.time() - float(self.args)}s") + + +class DummyRunnerCmdSet(CmdSet): + """ + Dummyrunner injected cmdset. + + """ + def at_cmdset_creation(self): + self.add(CmdDummyRunnerEchoResponse()) + # ------------------------------------------------------------ # Helper functions # ------------------------------------------------------------ @@ -181,12 +244,12 @@ def idcounter(): Makes unique ids. Returns: - count (int): A globally unique counter. + str: A globally unique id. """ global ICOUNT ICOUNT += 1 - return str(ICOUNT) + return str("{:03d}".format(ICOUNT)) GCOUNT = 0 @@ -202,7 +265,7 @@ def gidcounter(): """ global GCOUNT GCOUNT += 1 - return "%s-%s" % (time.strftime(DATESTRING), GCOUNT) + return "%s_%s" % (time.strftime(DATESTRING), GCOUNT) def makeiter(obj): @@ -222,7 +285,6 @@ def makeiter(obj): # Client classes # ------------------------------------------------------------ - class DummyClient(telnet.StatefulTelnetProtocol): """ Handles connection to a running Evennia server, @@ -231,22 +293,36 @@ class DummyClient(telnet.StatefulTelnetProtocol): """ + def report(self, text, clientkey): + pad = " " * (25 - len(text)) + tim = round(time.time() - self.connection_timestamp) + print(f"{text} {clientkey}{pad}\t" + f"conn: {NCONNECTED} -> " + f"welcome screen: {NLOGIN_SCREEN} -> " + f"authing: {NLOGGING_IN} -> " + f"loggedin/tot: {NLOGGED_IN}/{NCLIENTS} (after {tim}s)") + def connectionMade(self): """ Called when connection is first established. """ + global NCONNECTED # public properties self.cid = idcounter() - self.key = "Dummy-%s" % self.cid - self.gid = "%s-%s" % (time.strftime(DATESTRING), self.cid) + self.key = f"Dummy-{self.cid}" + self.gid = f"{time.strftime(DATESTRING)}_{self.cid}" self.istep = 0 self.exits = [] # exit names created self.objs = [] # obj names created + self.connection_timestamp = time.time() + self.connection_attempt = 0 + self.action_started = 0 self._connected = False self._loggedin = False self._logging_out = False + self._ready = False self._report = "" self._cmdlist = [] # already stepping in a cmd definition self._login = self.factory.actions[0] @@ -255,6 +331,43 @@ class DummyClient(telnet.StatefulTelnetProtocol): reactor.addSystemEventTrigger("before", "shutdown", self.logout) + NCONNECTED += 1 + self.report("-> connected", self.key) + + reactor.callLater(30, self._retry_welcome_screen) + + def _retry_welcome_screen(self): + if not self._connected and not self._ready: + # we have connected but not received anything for 30s. + # (unclear why this would be - overload?) + # try sending a look to get something to start with + self.report("?? retrying welcome screen", self.key) + self.sendLine(bytes("look", 'utf-8')) + # make sure to check again later + reactor.callLater(30, self._retry_welcome_screen) + + def _print_statistics(self): + global TIME_ALL_LOGIN, TOTAL_ACTIONS + global TOTAL_LAG, TOTAL_LAG_MEASURES, TOTAL_LAG_IN, TOTAL_LAG_OUT + + tim = time.time() - TIME_ALL_LOGIN + avgrate = round(TOTAL_ACTIONS / tim) + lag = TOTAL_LAG / (TOTAL_LAG_MEASURES or 1) + lag_in = TOTAL_LAG_IN / (TOTAL_LAG_MEASURES or 1) + lag_out = TOTAL_LAG_OUT / (TOTAL_LAG_MEASURES or 1) + + TOTAL_ACTIONS = 0 + TOTAL_LAG = 0 + TOTAL_LAG_IN = 0 + TOTAL_LAG_OUT = 0 + TOTAL_LAG_MEASURES = 0 + TIME_ALL_LOGIN = time.time() + + print(f".. running 30s average: ~{avgrate} actions/s " + f"lag: {lag:.2}s (in: {lag_in:.2}s, out: {lag_out:.2}s)") + + reactor.callLater(30, self._print_statistics) + def dataReceived(self, data): """ Called when data comes in over the protocol. We wait to start @@ -264,15 +377,67 @@ class DummyClient(telnet.StatefulTelnetProtocol): data (str): Incoming data. """ - if not self._connected and not data.startswith(chr(255)): - # wait until we actually get text back (not just telnet - # negotiation) - self._connected = True - # start client tick - d = LoopingCall(self.step) - # dissipate exact step by up to +/- 0.5 second - timestep = TIMESTEP + (-0.5 + (random.random() * 1.0)) - d.start(timestep, now=True).addErrback(self.error) + global NLOGIN_SCREEN, NLOGGED_IN, NLOGGING_IN, NCONNECTED + global TOTAL_ACTIONS, TIME_ALL_LOGIN + global TOTAL_LAG, TOTAL_LAG_MEASURES, TOTAL_LAG_IN, TOTAL_LAG_OUT + + if not data.startswith(b"\xff"): + # regular text, not a telnet command + + if NCLIENTS == 1: + print("dummy-client sees:", str(data, "utf-8")) + + if not self._connected: + # waiting for connection + # wait until we actually get text back (not just telnet + # negotiation) + # start client tick + d = LoopingCall(self.step) + df = max(abs(TIMESTEP * 0.001), min(TIMESTEP/10, 0.5)) + # dither next attempt with random time + timestep = TIMESTEP + (-df + (random.random() * df)) + d.start(timestep, now=True).addErrback(self.error) + self.connection_attempt += 1 + + self._connected = True + NLOGIN_SCREEN += 1 + NCONNECTED -= 1 + self.report("<- server sent login screen", self.key) + + elif self._loggedin: + if not self._ready: + # logged in, ready to run + NLOGGED_IN += 1 + NLOGGING_IN -= 1 + self._ready = True + self.report("== logged in", self.key) + if NLOGGED_IN == NCLIENTS and not TIME_ALL_LOGIN: + # all are logged in! We can start collecting statistics + print(".. All clients connected and logged in!") + TIME_ALL_LOGIN = time.time() + reactor.callLater(30, self._print_statistics) + + elif TIME_ALL_LOGIN: + TOTAL_ACTIONS += 1 + + try: + data = strip_ansi(str(data, "utf-8").strip()) + if data.startswith("dummyrunner_echo_response:"): + # handle special lag-measuring command. This returns + # dummyrunner_echo_response:, + now = time.time() + _, data = data.split(":", 1) + start_time, mid_time = (float(part) for part in data.split(",", 1)) + lag_in = mid_time - start_time + lag_out = now - mid_time + total_lag = now - start_time # full round-about time + + TOTAL_LAG += total_lag + TOTAL_LAG_IN += lag_in + TOTAL_LAG_OUT += lag_out + TOTAL_LAG_MEASURES += 1 + except Exception: + pass def connectionLost(self, reason): """ @@ -283,7 +448,7 @@ class DummyClient(telnet.StatefulTelnetProtocol): """ if not self._logging_out: - print("client %s(%s) lost connection (%s)" % (self.key, self.cid, reason)) + self.report("XX lost connection", self.key) def error(self, err): """ @@ -310,9 +475,9 @@ class DummyClient(telnet.StatefulTelnetProtocol): """ self._logging_out = True - cmd = self._logout(self) - print("client %s(%s) logout (%s actions)" % (self.key, self.cid, self.istep)) - self.sendLine(cmd) + cmd = self._logout(self)[0] + self.report(f"-> logout/disconnect ({self.istep} actions)", self.key) + self.sendLine(bytes(cmd, 'utf-8')) def step(self): """ @@ -321,7 +486,7 @@ class DummyClient(telnet.StatefulTelnetProtocol): all "intelligence" of the dummy client. """ - global NLOGGED_IN + global NLOGGING_IN, NLOGIN_SCREEN rand = random.random() @@ -329,11 +494,13 @@ class DummyClient(telnet.StatefulTelnetProtocol): # no commands ready. Load some. if not self._loggedin: - if rand < CHANCE_OF_LOGIN: + if rand < CHANCE_OF_LOGIN or NLOGGING_IN < 10: + # lower rate of logins, but not below 1 / s # get the login commands self._cmdlist = list(makeiter(self._login(self))) - NLOGGED_IN += 1 # this is for book-keeping - print("connecting client %s (%i/%i)..." % (self.key, NLOGGED_IN, NCLIENTS)) + NLOGGING_IN += 1 # this is for book-keeping + NLOGIN_SCREEN -= 1 + self.report("-> create/login", self.key) self._loggedin = True else: # no login yet, so cmdlist not yet set @@ -347,12 +514,26 @@ class DummyClient(telnet.StatefulTelnetProtocol): # at this point we always have a list of commands if rand < CHANCE_OF_ACTION: # send to the game - self.sendLine(str(self._cmdlist.pop(0))) + cmd = str(self._cmdlist.pop(0)) + + if cmd.startswith("dummyrunner_echo_response"): + # we need to set the timer element as close to + # the send as possible + cmd = cmd.format(timestamp=time.time()) + + self.sendLine(bytes(cmd, 'utf-8')) + self.action_started = time.time() self.istep += 1 + if NCLIENTS == 1: + print(f"dummy-client sent: {cmd}") -class DummyFactory(protocol.ClientFactory): + +class DummyFactory(protocol.ReconnectingClientFactory): protocol = DummyClient + initialDelay = 1 + maxDelay = 1 + noisy = False def __init__(self, actions): "Setup the factory base (shared by all clients)" @@ -397,7 +578,7 @@ def start_all_dummy_clients(nclients): # setting up all clients (they are automatically started) factory = DummyFactory(actions) for i in range(NCLIENTS): - reactor.connectTCP("localhost", TELNET_PORT, factory) + reactor.connectTCP("127.0.0.1", TELNET_PORT, factory) # start reactor reactor.run() @@ -422,12 +603,23 @@ if __name__ == "__main__": ) args = parser.parse_args() + nclients = int(args.nclients[0]) - print(INFO_STARTING.format(N=args.nclients[0])) + print(INFO_STARTING.format( + nclients=nclients, + port=TELNET_PORT, + idmapper_cache_size=IDMAPPER_CACHE_MAXSIZE, + timestep=TIMESTEP, + rate=1/TIMESTEP, + chance_of_login=CHANCE_OF_LOGIN * 100, + chance_of_action=CHANCE_OF_ACTION * 100, + avg_rate=(1 / TIMESTEP) * CHANCE_OF_ACTION, + avg_rate_total=(1 / TIMESTEP) * CHANCE_OF_ACTION * nclients + )) # run the dummyrunner - t0 = time.time() - start_all_dummy_clients(nclients=args.nclients[0]) + TIME_START = t0 = time.time() + start_all_dummy_clients(nclients=nclients) ttot = time.time() - t0 # output runtime diff --git a/evennia/server/profiling/dummyrunner_settings.py b/evennia/server/profiling/dummyrunner_settings.py index 39efe59ed5..a408ad9c1b 100644 --- a/evennia/server/profiling/dummyrunner_settings.py +++ b/evennia/server/profiling/dummyrunner_settings.py @@ -6,9 +6,9 @@ the actions available to dummy accounts. The settings are global variables: -- TIMESTEP - time in seconds between each 'tick' -- CHANCE_OF_ACTION - chance 0-1 of action happening -- CHANCE_OF_LOGIN - chance 0-1 of login happening +- TIMESTEP - time in seconds between each 'tick'. 1 is a good start. +- CHANCE_OF_ACTION - chance 0-1 of action happening. Default is 0.5. +- CHANCE_OF_LOGIN - chance 0-1 of login happening. 0.01 is a good number. - TELNET_PORT - port to use, defaults to settings.TELNET_PORT - ACTIONS - see below @@ -16,23 +16,25 @@ ACTIONS is a tuple ```python (login_func, logout_func, (0.3, func1), (0.1, func2) ... ) + ``` where the first entry is the function to call on first connect, with a chance of occurring given by CHANCE_OF_LOGIN. This function is usually responsible for logging in the account. The second entry is always called when the dummyrunner disconnects from the server and should -thus issue a logout command. The other entries are tuples (chance, +thus issue a logout command. The other entries are tuples (chance, func). They are picked randomly, their commonality based on the cumulative chance given (the chance is normalized between all options so if will still work also if the given chances don't add up to 1). -Since each function can return a list of game-command strings, each -function may result in multiple operations. + +The PROFILE variable define pre-made ACTION tuples for convenience. + +Each function should return an iterable of one or more command-call +strings (like "look here"), so each can group multiple command operations. An action-function is called with a "client" argument which is a -reference to the dummy client currently performing the action. It -returns a string or a list of command strings to execute. Use the -client object for optionally saving data between actions. +reference to the dummy client currently performing the action. The client object has the following relevant properties and methods: @@ -55,11 +57,15 @@ commands (such as creating an account and logging in). ---- """ +import random +import string + # Dummy runner settings # Time between each dummyrunner "tick", in seconds. Each dummy # will be called with this frequency. -TIMESTEP = 2 +TIMESTEP = 1 +# TIMESTEP = 0.025 # 40/s # Chance of a dummy actually performing an action on a given tick. # This spreads out usage randomly, like it would be in reality. @@ -68,7 +74,7 @@ CHANCE_OF_ACTION = 0.5 # Chance of a currently unlogged-in dummy performing its login # action every tick. This emulates not all accounts logging in # at exactly the same time. -CHANCE_OF_LOGIN = 1.0 +CHANCE_OF_LOGIN = 0.01 # Which telnet port to connect to. If set to None, uses the first # default telnet port of the running server. @@ -79,9 +85,10 @@ TELNET_PORT = None # some convenient templates -DUMMY_NAME = "Dummy-%s" -DUMMY_PWD = "password-%s" -START_ROOM = "testing_room_start_%s" +DUMMY_NAME = "Dummy_{gid}" +DUMMY_PWD = (''.join(random.choice(string.ascii_letters + string.digits) + for _ in range(20)) + "-{gid}") +START_ROOM = "testing_room_start_{gid}" ROOM_TEMPLATE = "testing_room_%s" EXIT_TEMPLATE = "exit_%s" OBJ_TEMPLATE = "testing_obj_%s" @@ -94,26 +101,27 @@ TOBJ_TYPECLASS = "contrib.tutorial_examples.red_button.RedButton" # login/logout - def c_login(client): "logins to the game" # we always use a new client name - cname = DUMMY_NAME % client.gid - cpwd = DUMMY_PWD % client.gid + cname = DUMMY_NAME.format(gid=client.gid) + cpwd = DUMMY_PWD.format(gid=client.gid) + room_name = START_ROOM.format(gid=client.gid) - # set up for digging a first room (to move to and keep the - # login room clean) - roomname = ROOM_TEMPLATE % client.counter() - exitname1 = EXIT_TEMPLATE % client.counter() - exitname2 = EXIT_TEMPLATE % client.counter() - client.exits.extend([exitname1, exitname2]) + # we assign the dummyrunner cmdsert to ourselves so # we can use special commands + add_cmdset = ( + "py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;" + "self.cmdset.add(DummyRunnerCmdSet, persistent=False)" + ) + # create character, log in, then immediately dig a new location and + # teleport it (to keep the login room clean) cmds = ( - "create %s %s" % (cname, cpwd), - "connect %s %s" % (cname, cpwd), - "@dig %s" % START_ROOM % client.gid, - "@teleport %s" % START_ROOM % client.gid, - "@dig %s = %s, %s" % (roomname, exitname1, exitname2), + f"create {cname} {cpwd}", + f"connect {cname} {cpwd}", + f"dig {room_name}", + f"teleport {room_name}", + add_cmdset, ) return cmds @@ -122,14 +130,16 @@ def c_login_nodig(client): "logins, don't dig its own room" cname = DUMMY_NAME % client.gid cpwd = DUMMY_PWD % client.gid - - cmds = ("create %s %s" % (cname, cpwd), "connect %s %s" % (cname, cpwd)) + cmds = ( + f"create {cname} {cpwd}", + f"connect {cname} {cpwd}" + ) return cmds def c_logout(client): "logouts of the game" - return "@quit" + return ("quit",) # random commands @@ -141,7 +151,7 @@ def c_looks(client): if not cmds: cmds = ["look %s" % exi for exi in client.exits] if not cmds: - cmds = "look" + cmds = ("look",) return cmds @@ -151,7 +161,7 @@ def c_examines(client): if not cmds: cmds = ["examine %s" % exi for exi in client.exits] if not cmds: - cmds = "examine me" + cmds = ("examine me",) return cmds @@ -163,7 +173,7 @@ def c_idles(client): def c_help(client): "reads help files" - cmds = ("help", "help @teleport", "help look", "help @tunnel", "help @dig") + cmds = ("help", "dummyrunner_echo_response",) return cmds @@ -173,7 +183,7 @@ def c_digs(client): exitname1 = EXIT_TEMPLATE % client.counter() exitname2 = EXIT_TEMPLATE % client.counter() client.exits.extend([exitname1, exitname2]) - return "@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2) + return ("@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),) def c_creates_obj(client): @@ -200,9 +210,7 @@ def c_creates_button(client): def c_socialize(client): "socializechats on channel" cmds = ( - "ooc Hello!", - "ooc Testing ...", - "ooc Testing ... times 2", + "pub Hello!", "say Yo!", "emote stands looking around.", ) @@ -212,81 +220,117 @@ def c_socialize(client): def c_moves(client): "moves to a previously created room, using the stored exits" cmds = client.exits # try all exits - finally one will work - return "look" if not cmds else cmds + return ("look",) if not cmds else cmds def c_moves_n(client): "move through north exit if available" - return "north" + return ("north",) def c_moves_s(client): "move through south exit if available" - return "south" + return ("south",) -# 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 -# 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. +def c_measure_lag(client): + """ + Special dummyrunner command, injected in c_login. It measures + response time. Including this in the ACTION tuple will give more + dummyrunner output about just how fast commands are being processed. + + The dummyrunner will treat this special and inject the + {timestamp} just before sending. + + """ + return ("dummyrunner_echo_response {timestamp}",) + + +# Action profile (required) + +# Some pre-made profiles to test. To make your own, just assign a tuple to ACTIONS. # +# idler - does nothing after logging in +# looker - just looks around +# normal_player - moves around, reads help, looks around (digs rarely) (spammy) +# normal_builder - digs now and then, examines, creates objects, moves +# heavy_builder - digs and creates a lot, moves and examines +# socializing_builder - builds a lot, creates help entries, moves, chat (spammy) +# only_digger - extreme builder that only digs room after room + +PROFILE = "normal_player" -# "normal builder" definitionj -# 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.3, c_moves)) -# "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.01, c_creates_button), -# (0.2, c_moves)) -# "passive account" definition -# ACTIONS = ( c_login, -# c_logout, -# (0.7, c_looks), -# #(0.1, c_examines), -# (0.3, c_help)) -# #(0.1, c_digs), -# #(0.1, c_creates_obj), -# #(0.1, c_creates_button), -# #(0.4, c_moves)) -# "inactive account" definition -# ACTIONS = (c_login_nodig, -# c_logout, -# (1.0, c_idles)) -# "normal account" definition -ACTIONS = (c_login, c_logout, (0.01, c_digs), (0.39, c_looks), (0.2, c_help), (0.4, c_moves)) -# walking tester. This requires a pre-made -# "loop" of multiple rooms that ties back -# to limbo (using @tunnel and @open) -# ACTIONS = (c_login_nodig, -# c_logout, -# (1.0, c_moves_n)) -# "socializing heavy builder" definition -# ACTIONS = (c_login, -# c_logout, -# (0.1, c_socialize), -# (0.1, c_looks), -# (0.2, c_help), -# (0.1, c_creates_obj), -# (0.2, c_digs), -# (0.3, c_moves)) -# "heavy digger memory tester" definition -# ACTIONS = (c_login, -# c_logout, -# (1.0, c_digs)) +if PROFILE == 'idler': + ACTIONS = ( + c_login, + c_logout, + (0.9, c_idles), + (0.1, c_measure_lag), + ) +elif PROFILE == 'looker': + ACTIONS = ( + c_login, + c_logout, + (0.8, c_looks), + (0.2, c_measure_lag) + ) +elif PROFILE == 'normal_player': + ACTIONS = ( + c_login, + c_logout, + (0.01, c_digs), + (0.29, c_looks), + (0.2, c_help), + (0.3, c_moves), + (0.2, c_socialize), + (0.1, c_measure_lag) + ) +elif PROFILE == 'normal_builder': + 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.2, c_moves) + (0.1, c_measure_lag) + ) +elif PROFILE == 'heavy_builder': + ACTIONS = ( + c_login, + c_logout, + (0.1, c_looks), + (0.1, c_examines), + (0.2, c_help), + (0.1, c_digs), + (0.1, c_creates_obj), + (0.2, c_moves), + (0.1, c_measure_lag) + ) +elif PROFILE == 'socializing_builder': + ACTIONS = ( + c_login, + c_logout, + (0.1, c_socialize), + (0.1, c_looks), + (0.1, c_help), + (0.1, c_creates_obj), + (0.2, c_digs), + (0.3, c_moves), + (0.1, c_measure_lag) + ) +elif PROFILE == 'only_digger': + ACTIONS = ( + c_login, + c_logout, + (0.9, c_digs), + (0.1, c_measure_lag) + ) + +else: + print("No dummyrunner ACTION profile defined.") + import sys + sys.exit() diff --git a/evennia/server/profiling/settings_mixin.py b/evennia/server/profiling/settings_mixin.py index 2070961a59..b1e20618dd 100644 --- a/evennia/server/profiling/settings_mixin.py +++ b/evennia/server/profiling/settings_mixin.py @@ -6,6 +6,7 @@ running dummyrunner, like this: Note that these mixin-settings are not suitable for production servers! + """ # the dummyrunner will check this variable to make sure @@ -17,3 +18,25 @@ DUMMYRUNNER_MIXIN = True PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) # make dummy clients able to test all commands PERMISSION_ACCOUNT_DEFAULT = "Developer" +# disable throttles which would otherwise block the runner +CREATION_THROTTLE_LIMIT = None +CREATION_THROTTLE_TIMEOUT = None +LOGIN_THROTTLE_LIMIT = None +LOGIN_THROTTLE_TIMEOUT = None +MAX_COMMAND_RATE = 100000 +MAX_CONNECTION_RATE = 100000 +MAX_CHAR_LIMIT = 100000 + +print(""" + Dummyrunner settings_mixin added (ONLY FOR PROFILING, NOT FOR PRODUCTION!) + + PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) + PERMISSION_ACCOUNT_DEFAULT = "Developer" + CREATION_THROTTLE_LIMIT = None + CREATION_THROTTLE_TIMEOUT = None + LOGIN_THROTTLE_LIMIT = None + LOGIN_THROTTLE_TIMEOUT = None + MAX_COMMAND_RATE = 100000 + MAX_CONNECTION_RATE = 100000 + MAX_CHAR_LIMIT = 100000 +""") diff --git a/evennia/server/throttle.py b/evennia/server/throttle.py index 65fa59d77d..737198a53b 100644 --- a/evennia/server/throttle.py +++ b/evennia/server/throttle.py @@ -26,7 +26,8 @@ class Throttle: Keyword Args: name (str): Name of this throttle. - limit (int): Max number of failures before imposing limiter + limit (int): Max number of failures before imposing limiter. If `None`, + the throttle is disabled. timeout (int): number of timeout seconds after max number of tries has been reached. cache_size (int): Max number of attempts to record per IP within a @@ -197,6 +198,10 @@ class Throttle: False otherwise. """ + if self.limit is None: + # throttle is disabled + return False + now = time.time() ip = str(ip) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 7870daef3a..1450fc6c52 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -697,7 +697,8 @@ PERMISSION_ACCOUNT_DEFAULT = "Player" CLIENT_DEFAULT_WIDTH = 78 # telnet standard height is 24; does anyone use such low-res displays anymore? CLIENT_DEFAULT_HEIGHT = 45 -# Set rate limits per-IP on account creations and login attempts +# Set rate limits per-IP on account creations and login attempts. Set limits +# to None to disable. CREATION_THROTTLE_LIMIT = 2 CREATION_THROTTLE_TIMEOUT = 10 * 60 LOGIN_THROTTLE_LIMIT = 5