Project

General

Profile

0001-utils-add-defer-module-to-run-things-later-31204.patch

Benjamin Dauvergne, 04 Oct 2019 02:04 AM

Download (6.64 KB)

View differences:

Subject: [PATCH 1/4] utils: add defer module to run things later (#31204)

 passerelle/utils/defer.py |  88 ++++++++++++++++++++++++++
 tests/test_defer.py       | 128 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 216 insertions(+)
 create mode 100644 passerelle/utils/defer.py
 create mode 100644 tests/test_defer.py
passerelle/utils/defer.py
1
# Copyright (C) 2019  Entr'ouvert
2
#
3
# This program is free software: you can redistribute it and/or modify it
4
# under the terms of the GNU Affero General Public License as published
5
# by the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16

  
17
import functools
18
import logging
19
import threading
20

  
21
import django.db
22

  
23

  
24
class DeferrableBarrier(threading.local):
25
    @property
26
    def stack(self):
27
        if not hasattr(self, '_stack'):
28
            self._stack = []
29
        return self._stack
30

  
31
    def __push(self):
32
        self.stack.append([])
33

  
34
    def defer(self, func, *args, **kwargs):
35
        if self.should_defer:
36
            self.stack[-1].append((func, args, kwargs))
37
        else:
38
            return func(*args, **kwargs)
39

  
40
    def __pop(self):
41
        return self.stack.pop()
42

  
43
    def __enter__(self):
44
        self.__push()
45
        return self
46

  
47
    def __exit__(self, exc_type, exc_value, exc_tb):
48
        for func, args, kwargs in self.__pop():
49
            try:
50
                func(*args, **kwargs)
51
            except Exception:
52
                logging.exception('failed to run deferrable function %s', func)
53

  
54
    @property
55
    def should_defer(self):
56
        return bool(self.stack)
57

  
58
    def deferrable(self, func=None, predicate=None):
59
        '''Automatically defer a function if dynamic scope is inside a deferrable_barrier.'''
60
        if not func:
61
            return functools.partial(self.deferrable, predicate=predicate)
62

  
63
        @functools.wraps(func)
64
        def f(*args, **kwargs):
65
            if not predicate or predicate():
66
                return self.defer(func, *args, **kwargs)
67
            else:
68
                return func(*args, **kwargs)
69
        return f
70

  
71
    def __call__(self, func):
72
        '''Wraps func in a deferrable_barrier scope.'''
73

  
74
        @functools.wraps(func)
75
        def f(*args, **kwargs):
76
            with self:
77
                return func(*args, **kwargs)
78
        return f
79

  
80

  
81
deferrable_barrier = DeferrableBarrier()
82

  
83

  
84
def is_in_transaction():
85
    return getattr(django.db.connection, 'in_atomic_block', False)
86

  
87
deferrable = deferrable_barrier.deferrable
88
deferrable_if_in_transaction = deferrable_barrier.deferrable(predicate=is_in_transaction)
tests/test_defer.py
1
# Copyright (C) 2019  Entr'ouvert
2
#
3
# This program is free software: you can redistribute it and/or modify it
4
# under the terms of the GNU Affero General Public License as published
5
# by the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16
import threading
17

  
18
from django.db import transaction
19

  
20
from passerelle.utils import defer
21

  
22
import pytest
23

  
24

  
25
def test_deferrable_barrier():
26
    x = []
27

  
28
    def f():
29
        x.append(1)
30

  
31
    assert not defer.deferrable_barrier.should_defer
32
    with defer.deferrable_barrier:
33
        assert defer.deferrable_barrier.should_defer
34
        defer.deferrable_barrier.defer(f)
35
        assert x == []
36
    assert not defer.deferrable_barrier.should_defer
37
    assert x == [1]
38

  
39

  
40
def test_deferrable_barrier_with_threading():
41
    x = []
42

  
43
    def f():
44
        x.append(1)
45

  
46
    assert not defer.deferrable_barrier.should_defer
47
    with defer.deferrable_barrier:
48
        defer.deferrable_barrier.defer(f)
49
        assert x == []
50
        t = threading.Thread(target=defer.deferrable_barrier.defer, args=(f,))
51
        assert x == []
52
        t.start()
53
        t.join()
54
        assert x == [1]
55
    assert not defer.deferrable_barrier.should_defer
56
    assert x == [1, 1]
57

  
58

  
59
def test_deferrable():
60
    x = []
61

  
62
    @defer.deferrable
63
    def f():
64
        x.append(1)
65

  
66
    f()
67
    assert x == [1]
68

  
69
    assert not defer.deferrable_barrier.should_defer
70
    with defer.deferrable_barrier:
71
        f()
72
        assert x == [1]
73
    assert not defer.deferrable_barrier.should_defer
74
    assert x == [1, 1]
75

  
76

  
77
def test_deferrable_with_threading():
78
    x = []
79

  
80
    @defer.deferrable
81
    def f():
82
        x.append(1)
83

  
84
    f()
85
    assert x == [1]
86

  
87
    assert not defer.deferrable_barrier.should_defer
88
    with defer.deferrable_barrier:
89
        f()
90
        assert x == [1]
91
        t = threading.Thread(target=f)
92
        t.start()
93
        t.join()
94
        assert x == [1, 1]
95
    assert x == [1, 1, 1]
96
    assert not defer.deferrable_barrier.should_defer
97

  
98

  
99
def test_deferrable_if_in_transaction(transactional_db):
100
    assert not defer.is_in_transaction()
101

  
102
    x = []
103

  
104
    @defer.deferrable_if_in_transaction
105
    def f():
106
        x.append(1)
107

  
108
    f()
109
    assert x == [1]
110

  
111
    with transaction.atomic():
112
        assert defer.is_in_transaction()
113
        f()
114
        assert x == [1, 1]
115

  
116
    with pytest.raises(Exception):
117
        with defer.deferrable_barrier:
118
            f()
119
            assert x == [1, 1, 1]
120
            try:
121
                with transaction.atomic():
122
                    f()
123
                    assert x == [1, 1, 1]
124
                    raise Exception
125
            finally:
126
                assert x == [1, 1, 1]
127
    assert x == [1, 1, 1, 1]
128

  
0
-