Projet

Général

Profil

0001-tests-add-api-tests-with-heavy-concurrency-53367.patch

Benjamin Dauvergne, 27 avril 2021 15:14

Télécharger (8,45 ko)

Voir les différences:

Subject: [PATCH 1/2] tests: add api tests with heavy concurrency (#53367)

 tests/test_api_concurrency.py | 209 ++++++++++++++++++++++++++++++++++
 1 file changed, 209 insertions(+)
 create mode 100644 tests/test_api_concurrency.py
tests/test_api_concurrency.py
1
import datetime
2
import multiprocessing
3
import random
4
import re
5
import statistics
6
import sys
7
import time
8
from urllib.parse import urljoin
9

  
10
import mock
11
import pytest
12
import requests
13
from django.contrib.auth.models import User
14
from django.utils.functional import cached_property
15

  
16
from chrono.agendas.models import Agenda, Desk, Event, MeetingType, TimePeriod
17

  
18

  
19
class TestMeetingsApi:
20
    concurrency = 4
21

  
22
    @pytest.fixture
23
    def app(self, settings, live_server, transactional_db):
24
        """Use the threaded WSGI server provided by live_server then create
25
        `self.concurrency` requests clients to produce contention during
26
        concurrent fillslot on a single agenda. transactional_db is mandatory
27
        for all threads and processes to see the same database state."""
28

  
29
        settings.REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [
30
            'rest_framework.authentication.SessionAuthentication'
31
        ]
32
        user = User(username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net')
33
        user.set_password('password')
34
        user.save()
35

  
36
        agenda = Agenda.objects.create(
37
            label='foo', kind='meetings', minimal_booking_delay=2, maximal_booking_delay=9
38
        )
39
        MeetingType.objects.create(agenda=agenda, label='mt30', duration=30)
40
        MeetingType.objects.create(agenda=agenda, label='mt15', duration=5)
41
        desk = Desk.objects.create(agenda=agenda, label='desk')
42

  
43
        for weekday in range(7):
44
            TimePeriod.objects.create(
45
                weekday=weekday,
46
                start_time=datetime.time(8, 0),
47
                end_time=datetime.time(12, 0),
48
                desk=desk,
49
            )
50
            TimePeriod.objects.create(
51
                weekday=weekday,
52
                start_time=datetime.time(14, 0),
53
                end_time=datetime.time(18, 0),
54
                desk=desk,
55
            )
56

  
57
        class App:
58
            # "simulate" webtest app with requests
59
            logged = False
60

  
61
            @cached_property
62
            def session(self):
63
                return requests.Session()
64

  
65
            def login(self):
66
                if self.logged:
67
                    return
68
                response = self.get('/login/')
69
                csrfmiddlewaretoken = re.search(r'csrfmiddlewaretoken.*value="([^"]*)', response.text).group(
70
                    1
71
                )
72
                response = self.post(
73
                    '/login/',
74
                    {
75
                        'csrfmiddlewaretoken': csrfmiddlewaretoken,
76
                        'username': 'john.doe',
77
                        'password': 'password',
78
                    },
79
                    allow_redirects=False,
80
                )
81
                assert response.status_code == 302
82
                assert self.session.cookies['sessionid']
83
                self.logged = True
84

  
85
            def get(self, url):
86
                resp = self.session.get(urljoin(str(live_server), url))
87
                resp.raise_for_status()
88
                return resp
89

  
90
            def post(self, url, *args, **kwargs):
91
                resp = self.session.post(urljoin(str(live_server), url), *args, **kwargs)
92
                resp.raise_for_status()
93
                return resp
94

  
95
        with mock.patch('rest_framework.authentication.SessionAuthentication.enforce_csrf'):
96
            # prevent CSRF check on SessionAuthentication
97
            yield App()
98

  
99
    def test_lots_of_clients(self, app, transactional_db):
100
        # Create a meeting of 30 minutes from 09:45 to 10:15 and look for available
101
        # 5 minutes meeting between 10:00 and 10:30, there should be only 3 if the
102
        # exclusion from 30 minutes event is enforced.
103

  
104
        app.login()
105
        app.session.close()
106

  
107
        class Counters:
108
            datetimes_timings_queue = multiprocessing.Queue()
109
            fillslot_timings_queue = multiprocessing.Queue()
110
            fillslot_failed = multiprocessing.Array('i', [0] * self.concurrency, lock=False)
111
            fillslot_success = multiprocessing.Array('i', [0] * self.concurrency, lock=False)
112

  
113
        def worker(i):
114
            mt_choices = ['mt15', 'mt30']
115

  
116
            datetimes_timings = []
117
            fillslot_timings = []
118
            while True:
119
                if not mt_choices:
120
                    break
121
                mt = random.choice(mt_choices)
122
                start = time.time()
123
                resp = app.get('/api/agenda/foo/meetings/%s/datetimes/?hide_disabled=true' % mt)
124
                datetimes_timings.append(time.time() - start)
125
                available = len(resp.json()['data'])
126
                if available == 0:
127
                    mt_choices.remove(mt)
128
                    continue
129
                choice = random.randint(0, available - 1)
130
                slot = resp.json()['data'][choice]
131
                start = time.time()
132
                resp = app.post(slot['api']['fillslot_url'])
133
                fillslot_timings.append(time.time() - start)
134
                if resp.json()['err']:
135
                    Counters.fillslot_failed[i] += 1
136
                else:
137
                    Counters.fillslot_success[i] += 1
138
                print(
139
                    'Thread',
140
                    i,
141
                    'choice',
142
                    slot['id'],
143
                    'fillslot_failed',
144
                    Counters.fillslot_failed[i],
145
                    'fillslot_success',
146
                    Counters.fillslot_success[i],
147
                    'available',
148
                    available,
149
                    'booked',
150
                    sum(Counters.fillslot_success),
151
                )
152
                # stop when we have done at least 100 bookings
153
                if sum(Counters.fillslot_success) > 200:
154
                    break
155
                # yeld to scheduler
156
                # time.sleep(0.0001)
157
            Counters.datetimes_timings_queue.put(datetimes_timings)
158
            Counters.fillslot_timings_queue.put(fillslot_timings)
159

  
160
        threads = [multiprocessing.Process(target=worker, args=(i,)) for i in range(self.concurrency)]
161
        start = time.time()
162
        for thread in threads:
163
            thread.start()
164
        for thread in threads:
165
            thread.join()
166
        datetimes_timings = []
167
        while not Counters.datetimes_timings_queue.empty():
168
            datetimes_timings.extend(Counters.datetimes_timings_queue.get())
169
        fillslot_timings = []
170
        while not Counters.fillslot_timings_queue.empty():
171
            fillslot_timings.extend(Counters.fillslot_timings_queue.get())
172
        duration = time.time() - start
173
        print('Fillslot failed', sum(Counters.fillslot_failed))
174
        print('Fillslot success', sum(Counters.fillslot_success))
175
        print(
176
            'Percent of failure',
177
            '%2.0d %%'
178
            % (
179
                sum(Counters.fillslot_failed)
180
                / float(sum(Counters.fillslot_failed) + sum(Counters.fillslot_success))
181
                * 100
182
            ),
183
        )
184
        print(
185
            'Fillslot per second', (sum(Counters.fillslot_failed) + sum(Counters.fillslot_success)) / duration
186
        )
187
        args = (
188
            'Datetimes',
189
            'avg duration',
190
            statistics.mean(datetimes_timings),
191
        )
192
        if sys.version_info >= (3, 8):
193
            args += (
194
                'quantiles',
195
                statistics.quantiles(datetimes_timings, n=10),
196
            )
197
        print(*args)
198
        args = (
199
            'Fillslot',
200
            'avg duration',
201
            statistics.mean(fillslot_timings),
202
        )
203
        if sys.version_info >= (3, 8):
204
            args += (
205
                'quantiles',
206
                statistics.quantiles(fillslot_timings, n=10),
207
            )
208
        print(*args)
209
        assert Event.objects.count() == sum(Counters.fillslot_success)
0
-