Skip to content

Commit

Permalink
update the public client to the 2021 project specification
Browse files Browse the repository at this point in the history
added the consistency test 
removing user service
  • Loading branch information
kPsarakis committed May 26, 2021
2 parents 86379ed + 9be45dd commit 5c9e512
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 130 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
__pycache__
consistency-test/tmp
72 changes: 61 additions & 11 deletions README.md
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
91 changes: 91 additions & 0 deletions consistency-test/locustfile.py
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
63 changes: 63 additions & 0 deletions consistency-test/populate.py
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")
43 changes: 43 additions & 0 deletions consistency-test/run_consistency_test.py
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")
91 changes: 91 additions & 0 deletions consistency-test/verify.py
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)}")
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
locust==1.5.3
requests==2.25.1

0 comments on commit 5c9e512

Please sign in to comment.