Projet

Général

Profil

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

Benjamin Dauvergne, 23 avril 2021 18:49

Télécharger (7,1 ko)

Voir les différences:

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

 tests/test_api_concurrency.py | 174 ++++++++++++++++++++++++++++++++++
 1 file changed, 174 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 statistics
5
import sys
6
import time
7
from urllib.parse import urljoin
8

  
9
import pytest
10
import requests
11
from django.contrib.auth.models import User
12

  
13
from chrono.agendas.models import Agenda, Desk, Event, MeetingType, TimePeriod
14

  
15

  
16
class TestMeetingsApi:
17
    concurrency = 30
18

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

  
26
        user = User(username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net')
27
        user.set_password('password')
28
        user.save()
29

  
30
        agenda = Agenda.objects.create(
31
            label='foo', kind='meetings', minimal_booking_delay=2, maximal_booking_delay=9
32
        )
33
        MeetingType.objects.create(agenda=agenda, label='mt30', duration=30)
34
        MeetingType.objects.create(agenda=agenda, label='mt15', duration=5)
35
        desk = Desk.objects.create(agenda=agenda, label='desk')
36

  
37
        for weekday in range(7):
38
            TimePeriod.objects.create(
39
                weekday=weekday,
40
                start_time=datetime.time(8, 0),
41
                end_time=datetime.time(12, 0),
42
                desk=desk,
43
            )
44
            TimePeriod.objects.create(
45
                weekday=weekday,
46
                start_time=datetime.time(14, 0),
47
                end_time=datetime.time(18, 0),
48
                desk=desk,
49
            )
50

  
51
        auth = ('john.doe', 'password')
52

  
53
        class App:
54
            # "simulate" webtest app with requests
55
            def get(self, url):
56
                resp = requests.get(urljoin(str(live_server), url), auth=auth)
57
                resp.raise_for_status()
58
                return resp
59

  
60
            def post(self, url):
61
                resp = requests.post(urljoin(str(live_server), url), auth=auth)
62
                resp.raise_for_status()
63
                return resp
64

  
65
        return App()
66

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

  
72
        class Counters:
73
            datetimes_timings_queue = multiprocessing.Queue()
74
            fillslot_timings_queue = multiprocessing.Queue()
75
            fillslot_failed = multiprocessing.Array('i', [0] * self.concurrency, lock=False)
76
            fillslot_success = multiprocessing.Array('i', [0] * self.concurrency, lock=False)
77

  
78
        def worker(i):
79
            mt_choices = ['mt15', 'mt30']
80

  
81
            datetimes_timings = []
82
            fillslot_timings = []
83
            while True:
84
                if not mt_choices:
85
                    break
86
                mt = random.choice(mt_choices)
87
                start = time.time()
88
                resp = app.get('/api/agenda/foo/meetings/%s/datetimes/?hide_disabled=true' % mt)
89
                datetimes_timings.append(time.time() - start)
90
                available = len(resp.json()['data'])
91
                if available == 0:
92
                    mt_choices.remove(mt)
93
                    continue
94
                choice = random.randint(0, available - 1)
95
                slot = resp.json()['data'][choice]
96
                start = time.time()
97
                resp = app.post(slot['api']['fillslot_url'])
98
                fillslot_timings.append(time.time() - start)
99
                if resp.json()['err']:
100
                    Counters.fillslot_failed[i] += 1
101
                else:
102
                    Counters.fillslot_success[i] += 1
103
                print(
104
                    'Thread',
105
                    i,
106
                    'choice',
107
                    slot['id'],
108
                    'fillslot_failed',
109
                    Counters.fillslot_failed[i],
110
                    'fillslot_success',
111
                    Counters.fillslot_success[i],
112
                    'available',
113
                    available,
114
                    'booked',
115
                    sum(Counters.fillslot_success),
116
                )
117
                # stop when we have done at least 100 bookings
118
                if sum(Counters.fillslot_success) > 100:
119
                    break
120
                # yeld to scheduler
121
                time.sleep(0.0001)
122
            Counters.datetimes_timings_queue.put(datetimes_timings)
123
            Counters.fillslot_timings_queue.put(fillslot_timings)
124

  
125
        threads = [multiprocessing.Process(target=worker, args=(i,)) for i in range(self.concurrency)]
126
        start = time.time()
127
        for thread in threads:
128
            thread.start()
129
        datetimes_timings = []
130
        for i in range(self.concurrency):
131
            datetimes_timings.extend(Counters.datetimes_timings_queue.get())
132
        fillslot_timings = []
133
        for i in range(self.concurrency):
134
            fillslot_timings.extend(Counters.fillslot_timings_queue.get())
135
        for thread in threads:
136
            thread.join()
137
        duration = time.time() - start
138
        print('Fillslot failed', sum(Counters.fillslot_failed))
139
        print('Fillslot success', sum(Counters.fillslot_success))
140
        print(
141
            'Percent of failure',
142
            '%2.0d %%'
143
            % (
144
                sum(Counters.fillslot_failed)
145
                / float(sum(Counters.fillslot_failed) + sum(Counters.fillslot_success))
146
                * 100
147
            ),
148
        )
149
        print(
150
            'Fillslot per second', (sum(Counters.fillslot_failed) + sum(Counters.fillslot_success)) / duration
151
        )
152
        args = (
153
            'Datetimes',
154
            'avg duration',
155
            statistics.mean(datetimes_timings),
156
        )
157
        if sys.version_info >= (3, 8):
158
            args += (
159
                'quantiles',
160
                statistics.quantiles(datetimes_timings, n=10),
161
            )
162
        print(*args)
163
        args = (
164
            'Fillslot',
165
            'avg duration',
166
            statistics.mean(fillslot_timings),
167
        )
168
        if sys.version_info >= (3, 8):
169
            args += (
170
                'quantiles',
171
                statistics.quantiles(fillslot_timings, n=10),
172
            )
173
        print(*args)
174
        assert Event.objects.count() == sum(Counters.fillslot_success)
0
-