gur111 icon

thread_pool_server_tests.py

gur111 | PRO | 06/19/21 11:56:43 AM UTC (Edited) | 0 ⭐ | 3341 👁️ | Never ⏰ | []
Python |

13.45 KB

|

None

|

0 👍

/

0 👎

import math
import os
import time
import unittest
import subprocess as sp
import asyncio
import requests_async as requests
from h11 import RemoteProtocolError
from threading import Lock
 
SERVER_PATH = os.path.realpath(os.path.join(os.path.curdir, '..', 'server'))
DYNAMIC_REQ_TIME = 0.2
DEFAULT_PORT = 8080
DEFAULT_THREAD_COUNT = 3
DEFAULT_QUEUE_SIZE = 7
DEFAULT_STATIC_PAGE = 'home.html'
DEFAULT_DYNAMIC_PAGE = 'output.cgi'
SERVER_ADDRESS = 'localhost'  # 'localhost'
 
 
class RequestResult:
    def __init__(self, req_ind, res=None, e=None):
        self.res = res
        self.e = e
        self.req_ind = req_ind
 
    def has_exception(self):
        return self.e is not None
 
    def is_exception_of_type(self, exc_type):
        return isinstance(self.e, exc_type)
 
 
class RequestsTest(unittest.TestCase):
    # noinspection HttpUrlsUsage
    def __init__(self, *args, queue_size=DEFAULT_QUEUE_SIZE, thread_count=DEFAULT_THREAD_COUNT, policy='dt', **kwargs):
        super().__init__(*args, **kwargs)
        self.req_ind_mutex = Lock()
        self.dyn_url = f'http://{SERVER_ADDRESS}:{DEFAULT_PORT}/{DEFAULT_DYNAMIC_PAGE}'
        self.static_url = f'http://{SERVER_ADDRESS}:{DEFAULT_PORT}/{DEFAULT_STATIC_PAGE}'
        self.not_found_url = f'http://{SERVER_ADDRESS}:{DEFAULT_PORT}/not_found'
        self.forbidden_url = f'http://{SERVER_ADDRESS}:{DEFAULT_PORT}/forbidden.cgi'
        self.queue_size = queue_size
        self.max_reqs = self.queue_size
        self.server_path = SERVER_PATH
        self.thread_count = thread_count
        self.policy = policy
        if policy == 'random':
            self.per_drop_size = math.ceil(0.25 * (self.queue_size-self.thread_count))
        elif policy in ['dt', 'dh']:
            self.per_drop_size = 1
        elif policy == 'block':
            self.per_drop_size = 0
 
        self.last_req_index = 0
 
    def setUp(self):
        print('Setting up test:')
        print(f'\tthread: {self.thread_count}')
        print(f'\tqueue size: {self.queue_size}')
        print(f'\tpolicy: {self.policy}')
        os.chdir(os.path.dirname(self.server_path))
        self.server = sp.Popen([self.server_path, f'{DEFAULT_PORT}', f'{self.thread_count}', f'{self.queue_size}', self.policy])
        print(f'Server status is: {self.server.poll()}')
        # input('Confirm open port and hit RETURN')
        # print('')
 
    def tearDown(self) -> None:
        self.server.terminate()
 
    async def make_req(self, url, method='get'):
        try:
            self.req_ind_mutex.acquire()
            req_ind = self.last_req_index + 1
            self.last_req_index += 1
            self.req_ind_mutex.release()
            req_headers = {'req_index': f'{req_ind}'}
            # arrival_time = time.time() * 1000  # in milliseconds
            req_ind = self.last_req_index
            if method == 'get':
                req = requests.get(url, headers=req_headers)
            elif method == 'post':
                req = requests.post(url, headers=req_headers)
            elif method == 'delete':
                req = requests.delete(url, headers=req_headers)
            else:
                self.fail('Unknown request method')
 
            # print(f'Awaiting request {req_ind}')
            response = await req
            headers = response.headers
            for header in headers:
                if header.lower().startswith('stat'):
                    headers[header] = headers[header][1:]
 
            # response_time = time.time() * 1000
            # self.assertAlmostEqual(arrival_time, float(response.headers['stat-req-arrival']), delta=min(500 * DYNAMIC_REQ_TIME, (response_time - arrival_time) * 0.2))
        except Exception as e:
            r = RequestResult(req_ind=req_ind, e=e)
            return r
        else:
            r = RequestResult(req_ind=req_ind, res=response)
            return r
 
    async def make_requests(self, url, total_reqs):
        print(f'Requesting url: {self.dyn_url}')
        self.last_req_index = 0
        tasks = []
        fail_expected_tasks = []
        thread_stats = [{'count': 0, 'dyn': 0, 'static': 0} for _ in range(self.thread_count)]
        expected_error_count = total_reqs - self.max_reqs + ((-(total_reqs - self.max_reqs)) % self.per_drop_size)
        expected_average_dispatch = DYNAMIC_REQ_TIME * float(total_reqs - expected_error_count - min(self.thread_count, self.max_reqs)) / self.thread_count / 2
 
        if self.policy == 'random':
            for _ in range(total_reqs):
                task = asyncio.ensure_future(self.make_req(url))
                tasks.append(task)
        elif self.policy == 'dt':
            for _ in range(total_reqs - expected_error_count):
                task = asyncio.ensure_future(self.make_req(url))
                tasks.append(task)
 
            for _ in range(expected_error_count):
                task = asyncio.ensure_future(self.make_req(url))
                fail_expected_tasks.append(task)
 
        elif self.policy == 'dh':
            for _ in range(expected_error_count):
                task = asyncio.ensure_future(self.make_req(url))
                fail_expected_tasks.append(task)
 
            for _ in range(total_reqs - expected_error_count):
                task = asyncio.ensure_future(self.make_req(url))
                tasks.append(task)
 
        responses = await asyncio.gather(*tasks, *fail_expected_tasks, return_exceptions=True)
 
        responses = sorted(responses, key=lambda x: x.req_ind)
 
        error_count = 0
        total_dispatch = 0
        for res in responses:
            if res.is_exception_of_type(RemoteProtocolError):
                error_count += 1
                continue
            elif res.has_exception():
                raise res.e
 
            res = res.res
            total_dispatch += float(res.headers['stat-req-dispatch'])
            count, dyn, static = int(res.headers['stat-thread-count']), int(res.headers['stat-thread-dynamic']), int(res.headers['stat-thread-static'])
 
            tid = int(res.headers['stat-thread-id'])
            thread_stats[tid]['count'] = max(count, thread_stats[tid]['count'])
            thread_stats[tid]['dyn'] = max(dyn, thread_stats[tid]['dyn'])
            thread_stats[tid]['static'] = max(static, thread_stats[tid]['static'])
            self.assertEqual(count, dyn, f'Mismatch total request and dynamic request. Total: {count}. Dynamic: {dyn}')
            self.assertEqual(static, 0, f'Unexpected static requests count. Expected {0}. Actual {static}')
            # self.assertAlmostEqual(float(res.headers['stat-req-arrival']), arrival_time)
 
        total_count = total_dyn = 0
 
        for stat in thread_stats:
            total_count += stat['count']
            total_dyn += stat['dyn']
 
        print(f'Requests succeeded: {total_count}')
        print(f'Requests failed: {error_count}')
 
        self.assertEqual(total_count, total_reqs - error_count)
        self.assertEqual(total_count, total_dyn)
 
        self.assertEqual(expected_error_count, error_count, f'Unexpected error count. Expected {expected_error_count}. Actual: {error_count}')
 
        self.assertNotEqual(0, total_reqs - error_count, 'No request succeeded')
        average_dispatch = total_dispatch / float(total_reqs - error_count)
        self.assertAlmostEqual(expected_average_dispatch, average_dispatch, delta=max(1, expected_average_dispatch * 0.3),
                               msg=f'Unexpected average dispatch time. Expected: {expected_average_dispatch}. Actual: {average_dispatch}')
 
        # TODO: Fix check. Should check that the correct requests failed for each policy type
        # if self.policy == 'dh':
        #     for res in responses[self.thread_count:self.thread_count + error_count]:
        #         self.assertIsInstance(res.e, RemoteProtocolError)
        # elif self.policy == 'dt':
        #     for res in responses[-error_count:]:
        #         self.assertIsInstance(res.e, RemoteProtocolError)
 
 
class TestDropTailRequests(RequestsTest):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, policy='dt', **kwargs)
 
    def test_drop_single(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs + 1))
 
    def test_drop_double_queue_size(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs + self.queue_size * 2))
 
 
class TestDropHeadRequests(RequestsTest):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, policy='dh', **kwargs)
 
    def test_drop_single(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs + 1))
 
    def test_drop_double_queue_size(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs + self.queue_size * 2))
 
 
class TestDropRandomRequests(RequestsTest):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, queue_size=16, policy='random', **kwargs)
 
    def test_single_drop_random(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs + 1))
 
    def test_double_drop_random(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs + 2 * int(0.25 * self.queue_size)))
 
    def test_no_drop(self):
        asyncio.run(self.make_requests(self.dyn_url, self.max_reqs))
 
 
class TestMultiThreaded(RequestsTest):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, queue_size=80, **kwargs)
 
    def test_time_full_queue(self):
        start_time = time.time()
        req_count = self.max_reqs
        asyncio.run(self.make_requests(self.dyn_url, req_count))
        run_time = time.time() - start_time
        expected_runtime = math.ceil(req_count / float(self.thread_count)) * DYNAMIC_REQ_TIME
        # This is optimal so it must be greater
        self.assertGreater(run_time, expected_runtime)
        self.assertLess(run_time, expected_runtime * 2)
 
    def test_better_with_more_threads(self):
        start_time = time.time()
        req_count = self.max_reqs
        asyncio.run(self.make_requests(self.dyn_url, req_count))
        few_threads_run_time = time.time() - start_time
 
        self.server.terminate()
        self.thread_count *= 3
        self.setUp()
 
        start_time = time.time()
        asyncio.run(self.make_requests(self.dyn_url, req_count))
        more_threads_run_time = time.time() - start_time
 
        self.assertTrue(2 * more_threads_run_time < few_threads_run_time < 3 * more_threads_run_time, "Performance doesn't scale as expected with amount of threads")
 
 
class TestStatusCodes(RequestsTest):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, thread_count=1, queue_size=1, **kwargs)
 
    async def _make_req(self, url, expected_status, stat_map, method='get'):
        task = asyncio.ensure_future(self.make_req(url, method=method))
 
        res = await asyncio.ensure_future(task)
 
        headers = res.res.headers
 
        for k in stat_map:
            self.assertIn(k, headers)
            if stat_map[k] is not None:
                self.assertAlmostEqual(float(headers[k]), stat_map[k], delta=0.03, msg=f'Unexpected value for {k}. Expected: {stat_map[k]}. Actual: {headers[k]}')
 
        self.assertEqual(expected_status, res.res.status_code, f'Unexpected status code. Expected: {expected_status}. Actual: {res.res.status_code}')
 
    def test_404(self):
        stat_map = {
            'stat-req-arrival': None,
            'stat-req-dispatch': 0.00004,
            'stat-thread-id': 0,
            'stat-thread-count': 1,
            'stat-thread-static': 0,
            'stat-thread-dynamic': 0
        }
 
        asyncio.run(self._make_req(self.not_found_url, 404, stat_map))
 
    def test_dynamic(self):
        stat_map = {
            'stat-req-arrival': None,
            'stat-req-dispatch': 0.00004,
            'stat-thread-id': 0,
            'stat-thread-count': 1,
            'stat-thread-static': 0,
            'stat-thread-dynamic': 1
        }
        asyncio.run(self._make_req(self.dyn_url, 200, stat_map))
 
    def test_static(self):
        stat_map = {
            'stat-req-arrival': None,
            'stat-req-dispatch': 0.00004,
            'stat-thread-id': 0,
            'stat-thread-count': 1,
            'stat-thread-static': 1,
            'stat-thread-dynamic': 0
        }
        asyncio.run(self._make_req(self.static_url, 200, stat_map))
 
    def test_forbidden(self):
        stat_map = {
            'stat-req-arrival': None,
            'stat-req-dispatch': 0.00004,
            'stat-thread-id': 0,
            'stat-thread-count': 1,
            'stat-thread-static': 0,
            'stat-thread-dynamic': 0
        }
        asyncio.run(self._make_req(self.forbidden_url, 403, stat_map))
 
    def test_post(self):
        stat_map = {
            'stat-req-arrival': None,
            'stat-req-dispatch': 0.00004,
            'stat-thread-id': 0,
            'stat-thread-count': 1,
            'stat-thread-static': 0,
            'stat-thread-dynamic': 0
        }
        asyncio.run(self._make_req(self.static_url, 501, stat_map, method='post'))
 
 
class TestQueueSmallerThan(RequestsTest):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, thread_count=5, queue_size=2, **kwargs)
 
    def test_small_queue_enough_threads(self):
        asyncio.run(self.make_requests(url=self.dyn_url, total_reqs=self.thread_count))
 
 
if __name__ == '__main__':
    unittest.main()
 

Comments