From 4f475c7697722e946e39e42f38f3dd03a95d8765 Mon Sep 17 00:00:00 2001 From: "matrix.org" Date: Tue, 12 Aug 2014 15:10:52 +0100 Subject: Reference Matrix Home Server --- synapse/util/__init__.py | 40 ++++++++++++++++ synapse/util/async.py | 22 +++++++++ synapse/util/distributor.py | 108 ++++++++++++++++++++++++++++++++++++++++++++ synapse/util/jsonobject.py | 98 ++++++++++++++++++++++++++++++++++++++++ synapse/util/lockutils.py | 67 +++++++++++++++++++++++++++ synapse/util/logutils.py | 65 ++++++++++++++++++++++++++ synapse/util/stringutils.py | 24 ++++++++++ 7 files changed, 424 insertions(+) create mode 100644 synapse/util/__init__.py create mode 100644 synapse/util/async.py create mode 100644 synapse/util/distributor.py create mode 100644 synapse/util/jsonobject.py create mode 100644 synapse/util/lockutils.py create mode 100644 synapse/util/logutils.py create mode 100644 synapse/util/stringutils.py (limited to 'synapse/util') diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py new file mode 100644 index 0000000000..5361cb7ec2 --- /dev/null +++ b/synapse/util/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import reactor + +import time + + +class Clock(object): + """A small utility that obtains current time-of-day so that time may be + mocked during unit-tests. + + TODO(paul): Also move the sleep() functionallity into it + """ + + def time(self): + """Returns the current system time in seconds since epoch.""" + return time.time() + + def time_msec(self): + """Returns the current system time in miliseconds since epoch.""" + return self.time() * 1000 + + def call_later(self, delay, callback): + return reactor.callLater(delay, callback) + + def cancel_call_later(self, timer): + timer.cancel() diff --git a/synapse/util/async.py b/synapse/util/async.py new file mode 100644 index 0000000000..e04db8e285 --- /dev/null +++ b/synapse/util/async.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer, reactor + + +def sleep(seconds): + d = defer.Deferred() + reactor.callLater(seconds, d.callback, seconds) + return d diff --git a/synapse/util/distributor.py b/synapse/util/distributor.py new file mode 100644 index 0000000000..32d19402b4 --- /dev/null +++ b/synapse/util/distributor.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from twisted.internet import defer + +import logging + + +logger = logging.getLogger(__name__) + + +class Distributor(object): + """A central dispatch point for loosely-connected pieces of code to + register, observe, and fire signals. + + Signals are named simply by strings. + + TODO(paul): It would be nice to give signals stronger object identities, + so we can attach metadata, docstrings, detect typoes, etc... But this + model will do for today. + """ + + def __init__(self): + self.signals = {} + self.pre_registration = {} + + def declare(self, name): + if name in self.signals: + raise KeyError("%r already has a signal named %s" % (self, name)) + + self.signals[name] = Signal(name) + + if name in self.pre_registration: + signal = self.signals[name] + for observer in self.pre_registration[name]: + signal.observe(observer) + + def observe(self, name, observer): + if name in self.signals: + self.signals[name].observe(observer) + else: + # TODO: Avoid strong ordering dependency by allowing people to + # pre-register observations on signals that don't exist yet. + if name not in self.pre_registration: + self.pre_registration[name] = [] + self.pre_registration[name].append(observer) + + def fire(self, name, *args, **kwargs): + if name not in self.signals: + raise KeyError("%r does not have a signal named %s" % (self, name)) + + return self.signals[name].fire(*args, **kwargs) + + +class Signal(object): + """A Signal is a dispatch point that stores a list of callables as + observers of it. + + Signals can be "fired", meaning that every callable observing it is + invoked. Firing a signal does not change its state; it can be fired again + at any later point. Firing a signal passes any arguments from the fire + method into all of the observers. + """ + + def __init__(self, name): + self.name = name + self.observers = [] + + def observe(self, observer): + """Adds a new callable to the observer list which will be invoked by + the 'fire' method. + + Each observer callable may return a Deferred.""" + self.observers.append(observer) + + def fire(self, *args, **kwargs): + """Invokes every callable in the observer list, passing in the args and + kwargs. Exceptions thrown by observers are logged but ignored. It is + not an error to fire a signal with no observers. + + Returns a Deferred that will complete when all the observers have + completed.""" + deferreds = [] + for observer in self.observers: + d = defer.maybeDeferred(observer, *args, **kwargs) + + def eb(failure): + logger.warning( + "%s signal observer %s failed: %r", + self.name, observer, failure, + exc_info=( + failure.type, + failure.value, + failure.getTracebackObject())) + deferreds.append(d.addErrback(eb)) + + return defer.DeferredList(deferreds) diff --git a/synapse/util/jsonobject.py b/synapse/util/jsonobject.py new file mode 100644 index 0000000000..190a80a322 --- /dev/null +++ b/synapse/util/jsonobject.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy + +class JsonEncodedObject(object): + """ A common base class for defining protocol units that are represented + as JSON. + + Attributes: + unrecognized_keys (dict): A dict containing all the key/value pairs we + don't recognize. + """ + + valid_keys = [] # keys we will store + """A list of strings that represent keys we know about + and can handle. If we have values for these keys they will be + included in the `dictionary` instance variable. + """ + + internal_keys = [] # keys to ignore while building dict + """A list of strings that should *not* be encoded into JSON. + """ + + required_keys = [] + """A list of strings that we require to exist. If they are not given upon + construction it raises an exception. + """ + + def __init__(self, **kwargs): + """ Takes the dict of `kwargs` and loads all keys that are *valid* + (i.e., are included in the `valid_keys` list) into the dictionary` + instance variable. + + Any keys that aren't recognized are added to the `unrecognized_keys` + attribute. + + Args: + **kwargs: Attributes associated with this protocol unit. + """ + for required_key in self.required_keys: + if required_key not in kwargs: + raise RuntimeError("Key %s is required" % required_key) + + self.unrecognized_keys = {} # Keys we were given not listed as valid + for k, v in kwargs.items(): + if k in self.valid_keys or k in self.internal_keys: + self.__dict__[k] = v + else: + self.unrecognized_keys[k] = v + + def get_dict(self): + """ Converts this protocol unit into a :py:class:`dict`, ready to be + encoded as JSON. + + The keys it encodes are: `valid_keys` - `internal_keys` + + Returns + dict + """ + d = { + k: _encode(v) for (k, v) in self.__dict__.items() + if k in self.valid_keys and k not in self.internal_keys + } + d.update(self.unrecognized_keys) + return copy.deepcopy(d) + + def get_full_dict(self): + d = { + k: v for (k, v) in self.__dict__.items() + if k in self.valid_keys or k in self.internal_keys + } + d.update(self.unrecognized_keys) + return copy.deepcopy(d) + + def __str__(self): + return "(%s, %s)" % (self.__class__.__name__, repr(self.__dict__)) + +def _encode(obj): + if type(obj) is list: + return [_encode(o) for o in obj] + + if isinstance(obj, JsonEncodedObject): + return obj.get_dict() + + return obj diff --git a/synapse/util/lockutils.py b/synapse/util/lockutils.py new file mode 100644 index 0000000000..e4d609d84e --- /dev/null +++ b/synapse/util/lockutils.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +import logging + + +logger = logging.getLogger(__name__) + + +class Lock(object): + + def __init__(self, deferred): + self._deferred = deferred + self.released = False + + def release(self): + self.released = True + self._deferred.callback(None) + + def __del__(self): + if not self.released: + logger.critical("Lock was destructed but never released!") + self.release() + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.release() + + +class LockManager(object): + """ Utility class that allows us to lock based on a `key` """ + + def __init__(self): + self._lock_deferreds = {} + + @defer.inlineCallbacks + def lock(self, key): + """ Allows us to block until it is our turn. + Args: + key (str) + Returns: + Lock + """ + new_deferred = defer.Deferred() + old_deferred = self._lock_deferreds.get(key) + self._lock_deferreds[key] = new_deferred + + if old_deferred: + yield old_deferred + + defer.returnValue(Lock(new_deferred)) diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py new file mode 100644 index 0000000000..08d5aafca4 --- /dev/null +++ b/synapse/util/logutils.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from inspect import getcallargs + +import logging + + +def log_function(f): + """ Function decorator that logs every call to that function. + """ + func_name = f.__name__ + lineno = f.func_code.co_firstlineno + pathname = f.func_code.co_filename + + def wrapped(*args, **kwargs): + name = f.__module__ + logger = logging.getLogger(name) + level = logging.DEBUG + + if logger.isEnabledFor(level): + bound_args = getcallargs(f, *args, **kwargs) + + def format(value): + r = str(value) + if len(r) > 50: + r = r[:50] + "..." + return r + + func_args = [ + "%s=%s" % (k, format(v)) for k, v in bound_args.items() + ] + + msg_args = { + "func_name": func_name, + "args": ", ".join(func_args) + } + + record = logging.LogRecord( + name=name, + level=level, + pathname=pathname, + lineno=lineno, + msg="Invoked '%(func_name)s' with args: %(args)s", + args=msg_args, + exc_info=None + ) + + logger.handle(record) + + return f(*args, **kwargs) + + return wrapped diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py new file mode 100644 index 0000000000..91550583a4 --- /dev/null +++ b/synapse/util/stringutils.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random +import string + + +def origin_from_ucid(ucid): + return ucid.split("@", 1)[1] + + +def random_string(length): + return ''.join(random.choice(string.ascii_letters) for _ in xrange(length)) -- cgit 1.4.1