-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
update the public client to the 2021 project specification
added the consistency test removing user service
- Loading branch information
Showing
9 changed files
with
496 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.idea | ||
__pycache__ | ||
consistency-test/tmp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,65 @@ | ||
# Instructions | ||
## Running locust | ||
* Install python 3.6 or greater | ||
* Install locust using either: | ||
* pip: `pip install locustio==0.14.6` | ||
* conda: `conda install locust` | ||
* Change the urls and ports in lines 8-11 in `locustfile.py` to correspond to your own | ||
|
||
## Setup | ||
* Install python 3.6 or greater (tested with 3.9) | ||
* Install the required packages using: `pip install -r requirements.txt` | ||
* Change the URLs and ports in the `urls.json` file with your own | ||
|
||
```` | ||
Note: For Windows users you might also need to install pywin32 | ||
```` | ||
|
||
## Stress Test | ||
|
||
In the provided stress test we have created 6 scenarios: | ||
|
||
1) A stock admin creates an item and adds stock to it | ||
|
||
2) A user checks out an order with one item inside that an admin has added stock to before | ||
|
||
3) A user checks out an order with two items inside that an admin has added stock to before | ||
|
||
4) A user adds an item to an order, regrets it and removes it and then adds it back and checks out | ||
|
||
5) Scenario that is supposed to fail because the second item does not have enough stock | ||
|
||
6) Scenario that is supposed to fail because the user does not have enough credit | ||
|
||
To change the weight (task frequency) of the provided scenarios you can change the weights in the `tasks` definition (line 358) | ||
With our locust file each user will make one request between 1 and 15 seconds (you can change that in line 356). | ||
|
||
``` | ||
YOU CAN ALSO CREATE YOUR OWN SCENARIOS AS YOU LIKE | ||
``` | ||
|
||
### Running | ||
* Open terminal and navigate to the `locustfile.py` folder | ||
* Run script: `locust -f locustfile.py --host=""` | ||
* Run script: `locust -f locustfile.py --host="localhost"` | ||
* Go to `http://localhost:8089/` | ||
## Using the Locust UI | ||
|
||
|
||
### Using the Locust UI | ||
Fill in an appropriate number of users that you want to test with. | ||
With our locust file each user will make one request between 1 and 15 seconds (you can change that in line 347). | ||
The hatch rate is how many users will spawn per second (locust suggests that you should use less than 100). | ||
Leave the host field empty it will automatically find the right one (it corresponds to the locust master host). | ||
The hatch rate is how many users will spawn per second | ||
(locust suggests that you should use less than 100 in local mode). | ||
|
||
|
||
## Consistency Test | ||
|
||
In the provided consistency test we first populate the databases with 100 items with 1 stock that costs 1 credit | ||
and 1000 users that have 1 credit. | ||
|
||
Then we concurrently send 1000 checkouts of 1 item with random user/item combinations. | ||
The probabilities say that only 10% of the checkouts will succeed, and the expected state should be 0 stock across all | ||
items and 100 credits subtracted from different users. | ||
|
||
Finally, the measurements are done in two phases: | ||
1) Using logs to see whether the service sent the correct message to the clients | ||
2) Querying the database to see if the actual state remained consistent | ||
|
||
### Running | ||
* Run script `run_consistency_test.py` | ||
|
||
### Interpreting Results | ||
|
||
Wait for the script to finish and check how many inconsistencies you have in both the payment and stock services |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import pickle | ||
import random | ||
import logging | ||
import json | ||
import os | ||
from typing import List, Union | ||
|
||
from locust import HttpUser, SequentialTaskSet, task, constant | ||
from locust.exception import StopUser | ||
|
||
with open(os.path.join('..', 'urls.json')) as f: | ||
urls = json.load(f) | ||
ORDER_URL = urls['ORDER_URL'] | ||
PAYMENT_URL = urls['PAYMENT_URL'] | ||
STOCK_URL = urls['STOCK_URL'] | ||
|
||
|
||
def create_order(session): | ||
with session.client.post(f"{ORDER_URL}/orders/create/{session.user_id}", json={}, name="/orders/create/[user_id]", | ||
catch_response=True) as response: | ||
try: | ||
session.order_id = response.json()['order_id'] | ||
except json.JSONDecodeError: | ||
response.failure("SERVER ERROR") | ||
|
||
|
||
def add_item_to_order(session, item_idx: int): | ||
with session.client.post(f"{ORDER_URL}/orders/addItem/{session.order_id}/{session.item_ids[item_idx]}", | ||
name="/orders/addItem/[order_id]/[item_id]", json={}, catch_response=True) as response: | ||
if 400 <= response.status_code < 500: | ||
response.failure(response.text) | ||
raise StopUser() | ||
else: | ||
response.success() | ||
|
||
|
||
def checkout_order(session): | ||
with session.client.post(f"{ORDER_URL}/orders/checkout/{session.order_id}", json={}, | ||
name="/orders/checkout/[order_id]", catch_response=True) as response: | ||
if 400 <= response.status_code < 500: | ||
logging.info(f"CHECKOUT | ORDER: {session.order_id} USER: {session.user_id} FAIL __OUR_LOG__") | ||
response.failure(response.text) | ||
else: | ||
logging.info(f"CHECKOUT | ORDER: {session.order_id} USER: {session.user_id} SUCCESS __OUR_LOG__") | ||
response.success() | ||
|
||
|
||
def load_pickle_file(file_name: str) -> Union[List[str], str]: | ||
with open(file_name, 'rb') as pkl_file: | ||
var = pickle.load(pkl_file) | ||
return var | ||
|
||
|
||
class ConsistencyTest(SequentialTaskSet): | ||
""" | ||
Scenario where a user checks out an order with one item inside that an admin has added stock to before | ||
""" | ||
|
||
order_id: str | ||
user_id: str | ||
user_ids: List[str] | ||
item_ids: List[str] | ||
|
||
def __init__(self, parent): | ||
super().__init__(parent) | ||
self.local_random = random.Random() | ||
self.user_ids = load_pickle_file('tmp/user_ids.pkl') | ||
tmp_item_ids = load_pickle_file('tmp/item_ids.pkl') | ||
self.item_ids = tmp_item_ids if type(tmp_item_ids) is list else [str(tmp_item_ids)] | ||
|
||
def on_start(self): | ||
self.user_id = str(self.local_random.choice(self.user_ids)) | ||
self.order_id = "" | ||
|
||
def on_stop(self): | ||
self.user_id = str(self.local_random.choice(self.user_ids)) | ||
self.order_id = "" | ||
|
||
@task | ||
def user_creates_order(self): create_order(self) | ||
|
||
@task | ||
def user_adds_item_to_order(self): add_item_to_order(self, 0) | ||
|
||
@task | ||
def user_checks_out_order(self): checkout_order(self) | ||
|
||
|
||
class MicroservicesUser(HttpUser): | ||
tasks = {ConsistencyTest: 100} | ||
wait_time = constant(1) # how much time a user waits (seconds) to run another SequentialTaskSet |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import pickle | ||
import json | ||
import logging | ||
import os | ||
from typing import Union, List | ||
|
||
import requests | ||
from multiprocessing.pool import ThreadPool | ||
from itertools import repeat | ||
|
||
logging.basicConfig(level=logging.INFO, | ||
format='%(levelname)s - %(asctime)s - %(name)s - %(message)s', | ||
datefmt='%I:%M:%S') | ||
logger = logging.getLogger(__name__) | ||
NUMBER_0F_ITEMS = 100 | ||
NUMBER_OF_USERS = 1000 | ||
|
||
with open(os.path.join('..', 'urls.json')) as f: | ||
urls = json.load(f) | ||
ORDER_URL = urls['ORDER_URL'] | ||
PAYMENT_URL = urls['PAYMENT_URL'] | ||
STOCK_URL = urls['STOCK_URL'] | ||
|
||
|
||
def create_user_offline(balance: int) -> str: | ||
user_id = requests.post(f"{PAYMENT_URL}/payment/create_user", json={}).json()['user_id'] | ||
requests.post(f"{PAYMENT_URL}/payment/add_funds/{user_id}/{balance}", json={}) | ||
return str(user_id) | ||
|
||
|
||
def create_item_offline(stock_to_add: int, price: int = 1) -> str: | ||
__item_id = requests.post(f"{STOCK_URL}/stock/item/create/{price}", json={}).json()['item_id'] | ||
requests.post(f"{STOCK_URL}/stock/add/{__item_id}/{stock_to_add}", json={}) | ||
return str(__item_id) | ||
|
||
|
||
def create_items_offline(number_of_items: int, stock: int = 1) -> List[str]: | ||
with ThreadPool(10) as pool: | ||
__item_ids = list(pool.map(create_item_offline, repeat(stock, number_of_items))) | ||
return __item_ids | ||
|
||
|
||
def create_users_offline(number_of_users: int, credit: int = 1) -> List[str]: | ||
with ThreadPool(10) as pool: | ||
__user_ids = list(pool.map(create_user_offline, repeat(credit, number_of_users))) | ||
return __user_ids | ||
|
||
|
||
def write_pickle(file_name: str, var: Union[List[str], str]): | ||
with open(file_name, 'wb') as output: | ||
pickle.dump(var, output, pickle.HIGHEST_PROTOCOL) | ||
|
||
|
||
def populate_databases(): | ||
logger.info("Creating items ...") | ||
item_id = create_item_offline(NUMBER_0F_ITEMS) # create item with 100 stock | ||
write_pickle('tmp/item_ids.pkl', item_id) | ||
logger.info("Items created") | ||
|
||
logger.info("Creating users ...") | ||
user_ids = create_users_offline(NUMBER_OF_USERS) # create 1000 users | ||
write_pickle('tmp/user_ids.pkl', user_ids) | ||
logger.info("Users created") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import os | ||
import shutil | ||
import subprocess | ||
import logging | ||
|
||
from verify import verify_systems_consistency | ||
from populate import populate_databases | ||
|
||
STRESS_TEST_EXECUTION_TIME = 30 # Seconds | ||
|
||
logging.basicConfig(level=logging.INFO, | ||
format='%(levelname)s - %(asctime)s - %(name)s - %(message)s', | ||
datefmt='%I:%M:%S') | ||
logger = logging.getLogger("Consistency test") | ||
|
||
|
||
# Create the tmp folder to store the logs, the users and the stock | ||
logger.info("Creating tmp folder...") | ||
current_file_directory: str = os.path.dirname(os.path.realpath(__file__)) | ||
tmp_folder_path: str = os.path.join(current_file_directory, 'tmp') | ||
tmp_folder_exists: bool = os.path.isdir(tmp_folder_path) | ||
|
||
if tmp_folder_exists: | ||
shutil.rmtree(tmp_folder_path) | ||
os.mkdir(tmp_folder_path) | ||
logger.info("tmp folder created") | ||
|
||
# Populate the payment and stock databases | ||
logger.info("Populating the databases...") | ||
populate_databases() | ||
logger.info("Databases populated") | ||
|
||
# Run the load test | ||
logger.info("Starting the load test...") | ||
subprocess.call(["locust", "-f", "locustfile.py", "--host=''", "--logfile=tmp/consistency-test.log", "--headless", | ||
"-u", "1000", "-r", "1000", f"--run-time={STRESS_TEST_EXECUTION_TIME}s"], | ||
stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) | ||
logger.info("Load test completed") | ||
|
||
# Verify the systems' consistency | ||
logger.info("Starting the consistency evaluation...") | ||
verify_systems_consistency() | ||
logger.info("Consistency evaluation completed") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import pickle | ||
import re | ||
import os | ||
import json | ||
import requests | ||
import logging | ||
from multiprocessing.pool import ThreadPool | ||
from typing import Union, List, Dict, Tuple | ||
|
||
from populate import NUMBER_0F_ITEMS | ||
|
||
CORRECT_USER_STATE = 900 | ||
|
||
logging.basicConfig(level=logging.INFO, | ||
format='%(levelname)s - %(asctime)s - %(name)s - %(message)s', | ||
datefmt='%I:%M:%S') | ||
logger = logging.getLogger(__name__) | ||
|
||
with open(os.path.join('..', 'urls.json')) as f: | ||
urls = json.load(f) | ||
ORDER_URL = urls['ORDER_URL'] | ||
PAYMENT_URL = urls['PAYMENT_URL'] | ||
STOCK_URL = urls['STOCK_URL'] | ||
|
||
|
||
def load_pickle_file(file_name: str) -> Union[List[str], str]: | ||
with open(file_name, 'rb') as pkl_file: | ||
var = pickle.load(pkl_file) | ||
return var | ||
|
||
|
||
def get_user_credit(user_id: str) -> Tuple[str, int]: | ||
credit = int(requests.get(f"{PAYMENT_URL}/payment/find_user/{user_id}", json={}).json()['credit']) | ||
return user_id, credit | ||
|
||
|
||
def get_user_credit_dict(user_id_list: List[str]) -> Dict[str, int]: | ||
with ThreadPool(10) as pool: | ||
user_id_credit = dict(pool.map(get_user_credit, user_id_list)) | ||
return user_id_credit | ||
|
||
|
||
def get_item_stock(item_id: str) -> Tuple[str, int]: | ||
stock = int(requests.get(f"{STOCK_URL}/stock/find/{item_id}", json={}).json()['stock']) | ||
return item_id, stock | ||
|
||
|
||
def get_item_stock_dict(item_id_list: Union[List[str], str]) -> Dict[str, int]: | ||
if type(item_id_list) is list: | ||
with ThreadPool(10) as pool: | ||
item_id_stock = dict(pool.map(get_item_stock, item_id_list)) | ||
else: | ||
item_id_stock = dict([get_item_stock(item_id_list)]) | ||
return item_id_stock | ||
|
||
|
||
def get_prior_user_state(): | ||
user_state = dict() | ||
for user_id in load_pickle_file('tmp/user_ids.pkl'): | ||
user_state[str(user_id)] = 1 | ||
return user_state | ||
|
||
|
||
def parse_log(prior_user_state: Dict[str, int]): | ||
i = 0 | ||
with open('tmp/consistency-test.log', 'r') as log_file: | ||
log_file = log_file.readlines() | ||
for log in log_file: | ||
if log.endswith('__OUR_LOG__\n'): | ||
m = re.search('ORDER: (.*) USER: (.*) (.*) __OUR_LOG__', log) | ||
user_id = str(m.group(2)) | ||
status = m.group(3) | ||
if status == 'SUCCESS': | ||
i += 1 | ||
if prior_user_state[user_id] == 0: | ||
logger.info("NEGATIVE") | ||
prior_user_state[user_id] = prior_user_state[user_id] - 1 | ||
logger.info(f"Stock service inconsistencies in the logs: {i - NUMBER_0F_ITEMS}") | ||
return prior_user_state | ||
|
||
|
||
def verify_systems_consistency(): | ||
pus: dict = parse_log(get_prior_user_state()) | ||
uic: dict = get_user_credit_dict(load_pickle_file('tmp/user_ids.pkl')) | ||
iis: dict = get_item_stock_dict(load_pickle_file('tmp/item_ids.pkl')) | ||
server_side_items_bought: int = 100 - list(iis.values())[0] | ||
logger.info(f"Stock service inconsistencies in the database: {server_side_items_bought - NUMBER_0F_ITEMS}") | ||
logged_user_credit: int = sum(pus.values()) | ||
logger.info(f"Payment service inconsistencies in the logs: {abs(CORRECT_USER_STATE - logged_user_credit)}") | ||
server_side_user_credit: int = sum(list(uic.values())) | ||
logger.info(f"Payment service inconsistencies in the database: {abs(CORRECT_USER_STATE - server_side_user_credit)}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
locust==1.5.3 | ||
requests==2.25.1 |
Oops, something went wrong.