File size: 2,839 Bytes
0815e01
 
 
 
 
 
 
 
 
 
 
 
 
b6f5bc4
0815e01
 
 
 
b6f5bc4
0815e01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""A store that remembers slow-changing reads (classes, members, roles, tests, assignments) for a short time.

Any change made through it to one of those forgets everything remembered at once. Rows come back as copies.
"""
from __future__ import annotations

import copy
import threading
import time

READS = frozenset({
    "classes", "classes_of_student", "classes_taught", "members", "get_class", "roles_of",
    "tests_of_classes", "get_test", "assignments_of_classes", "get_assignment", "all_students", "accounts", "curriculum",
    "subjects",
})
CHANGES = frozenset({
    "create_class", "update_class", "delete_class", "add_member", "remove_member", "give_role", "take_role", "link_user",
    "create_test", "update_test", "remove_test", "create_assignment", "remove_assignment", "set_lesson_dates",
    "create_subject", "update_subject", "delete_subject", "create_lesson", "save_lesson", "delete_lesson",
})


def _key(name: str, args: tuple, kwargs: dict):
    def freeze(v):
        if isinstance(v, (set, frozenset)):
            return tuple(sorted(v, key=repr))
        if isinstance(v, (list, tuple)):
            return tuple(freeze(x) for x in v)
        return v
    return name, freeze(args), tuple(sorted((k, freeze(v)) for k, v in kwargs.items()))


class CachedStore:
    def __init__(self, store, seconds: float, clock=time.monotonic) -> None:
        self._store, self._seconds, self._clock = store, seconds, clock
        self._lock = threading.Lock()
        self._kept: dict = {}            # key -> (expires, value)
        self._generation = 0             # bumped by every change, so a read that began before one is not kept

    def forget(self) -> None:
        with self._lock:
            self._kept.clear()
            self._generation += 1

    def __getattr__(self, name: str):
        target = getattr(self._store, name)
        if self._seconds <= 0 or not callable(target):
            return target
        if name in CHANGES:
            def change(*args, **kwargs):
                try:
                    return target(*args, **kwargs)
                finally:
                    self.forget()
            return change
        if name in READS:
            def read(*args, **kwargs):
                key = _key(name, args, kwargs)
                now = self._clock()
                with self._lock:
                    hit = self._kept.get(key)
                    if hit and hit[0] > now:
                        return copy.deepcopy(hit[1])
                    generation = self._generation
                value = target(*args, **kwargs)
                with self._lock:
                    if generation == self._generation:
                        self._kept[key] = (now + self._seconds, copy.deepcopy(value))
                return value
            return read
        return target