From 43161f892641b321549c35720c0c14ba15e6485a Mon Sep 17 00:00:00 2001 From: PCBZ Date: Tue, 18 Nov 2025 13:25:46 -0800 Subject: [PATCH 01/11] add generating users and their following --- scripts/generate_all_test_data.sh | 56 +- tests/push_fanout_results_5K.json | 46 -- tests/results_5k_push.html | 618 ----------------- tests/results_5k_push_300users.html | 618 ----------------- tests/results_pull_300users.html | 656 ------------------ .../following_distribution.png | Bin 0 -> 20835 bytes .../locust_push_fanout_test.py | 0 .../{ => timeline_retrieval}/requirements.txt | 4 + .../seed_followings_posts.py | 394 +++++++++++ 9 files changed, 437 insertions(+), 1955 deletions(-) delete mode 100644 tests/push_fanout_results_5K.json delete mode 100644 tests/results_5k_push.html delete mode 100644 tests/results_5k_push_300users.html delete mode 100644 tests/results_pull_300users.html create mode 100644 tests/timeline_retrieval/following_distribution.png rename tests/{ => timeline_retrieval}/locust_push_fanout_test.py (100%) rename tests/{ => timeline_retrieval}/requirements.txt (87%) create mode 100644 tests/timeline_retrieval/seed_followings_posts.py diff --git a/scripts/generate_all_test_data.sh b/scripts/generate_all_test_data.sh index 3dab79a..7403c79 100755 --- a/scripts/generate_all_test_data.sh +++ b/scripts/generate_all_test_data.sh @@ -174,20 +174,36 @@ if ! command -v python3 &> /dev/null; then exit 1 fi -# Check dependencies for User Service script -echo "" -echo -e "${BLUE}Checking Python dependencies...${NC}" -python3 -c "import aiohttp" 2>/dev/null -if [ $? -ne 0 ]; then - echo -e "${YELLOW}⚠️ aiohttp not installed, installing...${NC}" - pip3 install aiohttp -fi -python3 -c "import boto3" 2>/dev/null -if [ $? -ne 0 ]; then - echo -e "${YELLOW}⚠️ boto3 not installed, installing...${NC}" - pip3 install boto3 -fi +# Helper: create and activate venv inside a service scripts directory +create_and_activate_service_venv() { + local service_scripts_dir="$1" + local venv_dir="$service_scripts_dir/.venv" + local req_file="$service_scripts_dir/requirements.txt" + + echo "\nSetting up virtualenv in $service_scripts_dir" + if [ ! -d "$venv_dir" ]; then + python3 -m venv "$venv_dir" + fi + + # shellcheck source=/dev/null + . "$venv_dir/bin/activate" + python -m pip install --upgrade pip setuptools wheel + + if [ -f "$req_file" ]; then + echo "Installing packages from $req_file" + python -m pip install -r "$req_file" + else + echo "No requirements.txt in $service_scripts_dir, installing minimal packages" + python -m pip install aiohttp boto3 requests + fi +} + +deactivate_venv() { + if [ -n "${VIRTUAL_ENV-}" ]; then + deactivate || true + fi +} START_TIME=$(date +%s) @@ -197,12 +213,15 @@ echo -e "${BLUE}Step 1: Creating Users via User Service API${NC}" echo -e "${BLUE}================================================================${NC}" echo "" -# Run User Service script -cd "$PROJECT_ROOT/services/user-service/scripts" +# Run User Service script (ensure per-service venv and deps) +USER_SCRIPTS_DIR="$PROJECT_ROOT/services/user-service/scripts" +create_and_activate_service_venv "$USER_SCRIPTS_DIR" +cd "$USER_SCRIPTS_DIR" python3 generate_test_data.py \ "$NUM_USERS" \ --url "$BASE_URL" \ --concurrency "$CONCURRENCY" +deactivate_venv USER_EXIT_CODE=$? @@ -227,13 +246,16 @@ echo -e "${BLUE}Step 2: Creating Relationships via Social Graph DynamoDB${NC}" echo -e "${BLUE}================================================================${NC}" echo "" -# Run Social Graph script -cd "$PROJECT_ROOT/services/social-graph-services/scripts" +# Run Social Graph script (ensure per-service venv and deps) +SOCIAL_SCRIPTS_DIR="$PROJECT_ROOT/services/social-graph-services/scripts" +create_and_activate_service_venv "$SOCIAL_SCRIPTS_DIR" +cd "$SOCIAL_SCRIPTS_DIR" bash generate_and_load.sh \ --users "$NUM_USERS" \ --region "$AWS_REGION" \ --followers-table "$FOLLOWERS_TABLE" \ --following-table "$FOLLOWING_TABLE" +deactivate_venv SOCIAL_EXIT_CODE=$? diff --git a/tests/push_fanout_results_5K.json b/tests/push_fanout_results_5K.json deleted file mode 100644 index 78002b1..0000000 --- a/tests/push_fanout_results_5K.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "scale": "5K", - "total_users": 5000, - "total_requests": 1564, - "write_count": 1023, - "read_count": 541, - "error_count": 7691, - "duration_seconds": 480.07200598716736, - "throughput": { - "total_rps": 3.2578446160049723, - "write_rps": 2.13093033387026, - "read_rps": 1.1269142821347122 - }, - "write_latency": { - "avg": 2377.3863585929835, - "p50": 2198.051929473877, - "p95": 4742.579221725464, - "p99": 6315.847873687744, - "max": 8410.406827926636 - }, - "read_latency": { - "avg": 312.35461887282054, - "p50": 44.82769966125488, - "p95": 126.83916091918945, - "p99": 8177.561044692993, - "max": 13163.871049880981 - }, - "regular_metrics": { - "write_count": 1023, - "read_count": 541, - "avg_write_latency": 2377.3863585929835, - "avg_read_latency": 312.35461887282054 - }, - "influencer_metrics": { - "write_count": 0, - "read_count": 0, - "avg_write_latency": 0, - "avg_read_latency": 0 - }, - "celebrity_metrics": { - "write_count": 0, - "read_count": 0, - "avg_write_latency": 0, - "avg_read_latency": 0 - } -} \ No newline at end of file diff --git a/tests/results_5k_push.html b/tests/results_5k_push.html deleted file mode 100644 index 9c561bb..0000000 --- a/tests/results_5k_push.html +++ /dev/null @@ -1,618 +0,0 @@ - - - - Test Report for locust_push_fanout_test.py - - - - -
-

Locust Test Report

- -
- -

During: 2025-11-12 04:46:29 - 2025-11-12 04:54:29

-

Target Host: http://cs6650-project-dev-alb-2009030594.us-west-2.elb.amazonaws.com

-

Script: locust_push_fanout_test.py

-
- -
-

Request Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GETGET /api/timeline248560214249653404651.70.0
POSTPOST /api/posts276502132662351965.80.0
Aggregated276210214249653366157.50.0
-
- -
-

Response Time Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GETGET /api/timeline728410015039079031009700
POSTPOST /api/posts52586911032099036006200
Aggregated698210014038081032009700
-
- - - - - - -
-

Charts

-
- - -
-

Final ratio

-
-
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/results_5k_push_300users.html b/tests/results_5k_push_300users.html deleted file mode 100644 index d809bab..0000000 --- a/tests/results_5k_push_300users.html +++ /dev/null @@ -1,618 +0,0 @@ - - - - Test Report for locust_push_fanout_test.py - - - - -
-

Locust Test Report

- -
- -

During: 2025-11-12 05:01:19 - 2025-11-12 05:09:19

-

Target Host: http://cs6650-project-dev-alb-2009030594.us-west-2.elb.amazonaws.com

-

Script: locust_push_fanout_test.py

-
- -
-

Request Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GETGET /api/timeline53921032328177844243112.30.0
POSTPOST /api/posts59780253271054619612.50.0
Aggregated59899031627177843839124.80.0
-
- -
-

Response Time Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GETGET /api/timeline821001302206001400420018000
POSTPOST /api/posts5666891705501300370011000
Aggregated80981302205901400420018000
-
- - - - - - -
-

Charts

-
- - -
-

Final ratio

-
-
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/results_pull_300users.html b/tests/results_pull_300users.html deleted file mode 100644 index 92cfa3a..0000000 --- a/tests/results_pull_300users.html +++ /dev/null @@ -1,656 +0,0 @@ - - - - Test Report for locust_push_fanout_test.py - - - - -
-

Locust Test Report

- -
- -

During: 2025-11-12 06:43:38 - 2025-11-12 06:51:38

-

Target Host: http://cs6650-project-dev-alb-2009030594.us-west-2.elb.amazonaws.com

-

Script: locust_push_fanout_test.py

-
- -
-

Request Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GETGET /api/timeline823076891376131154173117.116.0
POSTPOST /api/posts102522392114100511912.10.0
Aggregated925576911250231154174919.316.0
-
- -
-

Response Time Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GETGET /api/timeline1500015000150001500015000150001500015000
POSTPOST /api/posts220025002900330041004800640010000
Aggregated1500015000150001500015000150001500015000
-
- - -
-

Failures Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodNameErrorOccurrences
GETGET /api/timelineGot status code 5047474
POSTPOST /api/postsGot status code 5032
GETGET /api/timelineGot status code 500215
-
- - - - - -
-

Charts

-
- - -
-

Final ratio

-
-
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/timeline_retrieval/following_distribution.png b/tests/timeline_retrieval/following_distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..4b04d72d88323ede2bcafc2f00915f1c62b87ce4 GIT binary patch literal 20835 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�idi%OTd11#%$`Ls10gN*dZp(;XB^T!ugyc8fp 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 Optional[str]: + """Create and save a histogram of following counts. Returns path on success or None.""" + counts = [] + for it in items: + following = it.get("following_ids", []) or [] + if isinstance(following, dict): + # unexpected structure + continue + counts.append(len(following)) + + if not counts: + logger.info("No following counts to plot") + return None + + plt.figure(figsize=(10, 6)) + # Use log scale for y to show long tail + plt.hist(counts, bins=50, log=True, color="#2c7fb8", edgecolor="black") + plt.xlabel("Number of followings") + plt.ylabel("Number of users (log scale)") + plt.title("Distribution of following counts") + plt.grid(axis='y', alpha=0.6) + plt.tight_layout() + try: + plt.savefig(out_path) + logger.info(f"Saved following distribution plot to: {out_path}") + plt.close() + return out_path + except Exception as e: + logger.warning(f"Failed to save plot: {e}") + return None + + +def get_alb_url_from_terraform() -> Optional[str]: + """Search up the directory tree for a `terraform` directory and run + `terraform output -raw alb_dns_name`. Returns full http:// URL or None. + """ + cur = os.path.dirname(os.path.abspath(__file__)) + for _ in range(6): + terraform_dir = os.path.join(cur, 'terraform') + if os.path.isdir(terraform_dir): + try: + result = subprocess.run( + ['terraform', 'output', '-raw', 'alb_dns_name'], + cwd=terraform_dir, + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0 and result.stdout.strip(): + alb_dns = result.stdout.strip() + return f"http://{alb_dns}" + except Exception: + return None + parent = os.path.dirname(cur) + if parent == cur: + break + cur = parent + return None + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("seed_followings_posts") + +# Configuration - hardcoded in source as requested +# (LIMIT_FOLLOWINGS, WORKERS and FORCE were removed per request) + + +def scan_table(table) -> List[dict]: + items = [] + resp = table.scan() + items.extend(resp.get("Items", [])) + while "LastEvaluatedKey" in resp: + resp = table.scan(ExclusiveStartKey=resp["LastEvaluatedKey"]) + items.extend(resp.get("Items", [])) + return items + + +def select_target_users(items: List[dict]) -> Tuple[int, int, int]: + user_following_counts = [] # (user_id, following_count) + for it in items: + uid_raw = it.get("user_id") + try: + uid = int(uid_raw) + except Exception: + # try stringified digits + try: + uid = int(str(uid_raw)) + except Exception: + continue + following = it.get("following_ids", []) or [] + # ensure list-like + if isinstance(following, dict): + following = [] + user_following_counts.append((uid, len(following))) + + if not user_following_counts: + raise RuntimeError("No items found in following table") + + user_following_counts.sort(key=lambda x: x[1], reverse=True) + + max_user, max_count = user_following_counts[0] + + # find eq10 + user_eq_10 = None + for uid, cnt in reversed(user_following_counts): + if cnt == 10: + user_eq_10 = uid + user_eq_10_count = cnt + break + if user_eq_10 is None: + user_eq_10 = min(user_following_counts, key=lambda x: abs(x[1] - 10))[0] + user_eq_10_count = next(cnt for uid, cnt in user_following_counts if uid == user_eq_10) + + # find medium 100-500 + user_medium = None + for uid, cnt in user_following_counts: + if 100 <= cnt <= 500: + user_medium = uid + user_medium_count = cnt + break + if user_medium is None: + mid_idx = len(user_following_counts) // 2 + user_medium = user_following_counts[mid_idx][0] + user_medium_count = user_following_counts[mid_idx][1] + print(f"Selected users: max_user={max_user} ({max_count} followings), user_eq_10={user_eq_10} ({user_eq_10_count} followings), user_medium={user_medium} ({user_medium_count} followings)") + return max_user, user_eq_10, user_medium + + +def fetch_following_ids(table, user_id: int) -> List[int]: + # DynamoDB key may be stored as string; try both + for key in (str(user_id), user_id): + try: + resp = table.get_item(Key={"user_id": key}) + item = resp.get("Item") + if not item: + continue + following = item.get("following_ids", []) or [] + res = [] + for fid in following: + try: + res.append(int(fid)) + except Exception: + # skip unparsable + continue + return res + except Exception: + continue + return [] + + +def trim_following_to_limit(table, user_id: int, limit: int = 10) -> Tuple[bool, int]: + """If the user's following list has more than `limit` entries, truncate it to `limit` and write back to DynamoDB. + + Returns (changed, new_length) + """ + # Try to fetch item with string key then numeric key + for key in (str(user_id), user_id): + try: + resp = table.get_item(Key={"user_id": key}) + item = resp.get("Item") + if not item: + continue + + # Determine attribute name used for followings + if 'following' in item and isinstance(item['following'], list): + attr = 'following' + elif 'following_ids' in item and isinstance(item['following_ids'], list): + attr = 'following_ids' + else: + # nothing to trim + return False, 0 + + current = item.get(attr, []) or [] + if len(current) <= limit: + return False, len(current) + + new_list = current[:limit] + # Write back using UpdateExpression + table.update_item( + Key={"user_id": key}, + UpdateExpression=f"SET {attr} = :vals", + ExpressionAttributeValues={':vals': new_list} + ) + logger.info(f"Trimmed user {user_id} {attr} from {len(current)} -> {len(new_list)}") + return True, len(new_list) + except Exception as e: + logger.debug(f"trim_following_to_limit: get/update attempt for key={key} failed: {e}") + continue + + return False, 0 + + +def prepare_three_targets(region: str = "us-west-2", following_table_name: str = "social-graph-following") -> Tuple[int, int, int]: + """Scan following table, plot distribution, select three target users and trim them. + + Returns (table, base_url, max_user, user_eq_10, user_medium) + """ + base_url = get_alb_url_from_terraform() + if not base_url: + raise RuntimeError("ALB URL could not be found from Terraform output or ALB_URL env var") + + dynamodb = boto3.resource("dynamodb", region_name=region) + table = dynamodb.Table(following_table_name) + + logger.info("Scanning following table (this may take a while)...") + items = scan_table(table) + logger.info(f"Scanned {len(items)} items from {following_table_name}") + + max_user, user_eq_10, user_medium = select_target_users(items) + logger.info(f"Selected users: max={max_user}, eq10={user_eq_10}, medium={user_medium}") + + # Trim eq10 to 10 and medium to 100 + changed, new_len = trim_following_to_limit(table, user_eq_10, limit=10) + if changed: + logger.info(f"Trimmed user_eq_10 ({user_eq_10}) followings down to {new_len}") + time.sleep(0.5) + + changed_mid, new_len_mid = trim_following_to_limit(table, user_medium, limit=100) + if changed_mid: + logger.info(f"Trimmed user_medium ({user_medium}) followings down to {new_len_mid}") + time.sleep(0.5) + + return max_user, user_eq_10, user_medium + + +def post_for_user(session: requests.Session, base_url: str, user_id: int, posts_per_user: int = 10) -> int: + """Create posts_per_user posts for user_id. Returns number of successful posts.""" + success = 0 + for i in range(posts_per_user): + payload = {"user_id": user_id, "content": f"Auto-seed post {i+1} from user {user_id}"} + try: + r = session.post(f"{base_url.rstrip('/')}/api/posts", json=payload, timeout=10) + if r.status_code == 200: + success += 1 + else: + logger.debug(f"Post by {user_id} returned {r.status_code}") + except Exception as e: + logger.debug(f"HTTP error posting for {user_id}: {e}") + return success + + +def seed_for_target(table, base_url: str, target_uid: int, workers: int) -> Tuple[int, int, int]: + """For a given target user, fetch followings and have each following create posts. + Returns (target_uid, total_followings_processed, total_successful_posts) + """ + following_ids = fetch_following_ids(table, target_uid) + total_followings = len(following_ids) + + logger.info(f"Target {target_uid}: processing {len(following_ids)} followings (total in table: {total_followings})") + + session = requests.Session() + total_success = 0 + + # Use threadpool to parallelize per-following posting jobs + with ThreadPoolExecutor(max_workers=workers) as ex: + futures = {ex.submit(post_for_user, session, base_url, fid, 10): fid for fid in following_ids} + for fut in as_completed(futures): + fid = futures[fut] + try: + ok = fut.result() + total_success += ok + except Exception as e: + logger.debug(f"Error seeding for following {fid}: {e}") + + return target_uid, len(following_ids), total_success + + +def main(): + # Configuration comes from module-level constants + # Always read ALB URL from Terraform output + base_url = get_alb_url_from_terraform() + if not base_url: + logger.error("ALB URL could not be found from Terraform output (alb_dns_name). Ensure Terraform output exists or run terraform in a parent directory.") + return 2 + + region = "us-west-2" + following_table_name = "social-graph-following" + + logger.info(f"Using ALB URL: {base_url}") + logger.info(f"Region: {region}") + logger.info(f"Following table: {following_table_name}") + + dynamodb = boto3.resource("dynamodb", region_name=region) + table = dynamodb.Table(following_table_name) + + logger.info("Scanning following table (this may take a while)...") + items = scan_table(table) + logger.info(f"Scanned {len(items)} items from {following_table_name}") + + # Draw and save following distribution plot + try: + plot_path = plot_following_distribution(items) + if plot_path: + logger.info(f"Distribution plot created: {plot_path}") + except Exception as e: + logger.debug(f"Plotting failed: {e}") + + max_user, user_eq_10, user_medium = select_target_users(items) + logger.info(f"Selected users: max={max_user}, eq10={user_eq_10}, medium={user_medium}") + + # Safety check + # Ensure user_eq_10 has at most 10 followings in the DB; if not, trim it. + try: + changed, new_len = trim_following_to_limit(table, user_eq_10, limit=10) + if changed: + logger.info(f"Trimmed user_eq_10 ({user_eq_10}) followings down to {new_len}") + # allow small pause for DynamoDB eventual consistency + time.sleep(0.5) + except Exception as e: + logger.warning(f"Failed to trim followings for {user_eq_10}: {e}") + + # Ensure user_medium has at most 100 followings; trim if necessary + try: + changed_mid, new_len_mid = trim_following_to_limit(table, user_medium, limit=100) + if changed_mid: + logger.info(f"Trimmed user_medium ({user_medium}) followings down to {new_len_mid}") + time.sleep(0.5) + except Exception as e: + logger.warning(f"Failed to trim followings for medium user {user_medium}: {e}") + + total_followings = 0 + for uid in (max_user, user_eq_10, user_medium): + fids = fetch_following_ids(table, uid) + total_followings += len(fids) + + estimated_posts = total_followings * 10 + logger.info(f"Estimated total posts to create (all followings x10): {estimated_posts}") + # Default behavior: do not proceed automatically for very large jobs + if estimated_posts > 100000: + logger.warning("This job would create more than 10k posts. Edit the script to change the limit or scope if you really want to proceed.") + return 3 + + # Seed for each target + results = [] + start = time.time() + for uid in (max_user, user_eq_10, user_medium): + # Inline defaults: no cap on followings, 20 workers + res = seed_for_target(table, base_url, uid, 20) + results.append(res) + + duration = time.time() - start + + logger.info("Seeding summary:") + for target_uid, processed, successes in results: + logger.info(f"Target {target_uid}: processed followings={processed}, successful_posts={successes}") + + logger.info(f"Total time: {duration:.1f}s") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 8387d69431fa8a1240c67b52a93eeb8b85529914 Mon Sep 17 00:00:00 2001 From: PCBZ Date: Tue, 18 Nov 2025 22:00:43 -0800 Subject: [PATCH 02/11] add tests for timeline --- .../Report_10_Followings.html | 592 ++++++++++++++++++ .../locust_background_traffic.py | 93 +++ ...h_fanout_test.py => locust_fanout_test.py} | 0 .../locust_timeline_retrieve.py | 75 +++ tests/timeline_retrieval/requirements.txt | 7 +- .../run_locust_background.sh | 60 ++ .../run_locust_timeline_retrieve.sh | 133 ++++ .../seed_followings_posts.py | 34 +- 8 files changed, 981 insertions(+), 13 deletions(-) create mode 100644 tests/timeline_retrieval/Report_10_Followings.html create mode 100644 tests/timeline_retrieval/locust_background_traffic.py rename tests/timeline_retrieval/{locust_push_fanout_test.py => locust_fanout_test.py} (100%) create mode 100644 tests/timeline_retrieval/locust_timeline_retrieve.py create mode 100644 tests/timeline_retrieval/run_locust_background.sh create mode 100644 tests/timeline_retrieval/run_locust_timeline_retrieve.sh diff --git a/tests/timeline_retrieval/Report_10_Followings.html b/tests/timeline_retrieval/Report_10_Followings.html new file mode 100644 index 0000000..e4b626d --- /dev/null +++ b/tests/timeline_retrieval/Report_10_Followings.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-19 05:50:52 - 2025-11-19 05:51:50

+

Target Host: http://cs6650-project-dev-alb-1624724957.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline190748493167734650.30.0
Aggregated190748493167734650.30.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline7108008208901000170017001700
Aggregated7108008208901000170017001700
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/locust_background_traffic.py b/tests/timeline_retrieval/locust_background_traffic.py new file mode 100644 index 0000000..cc9ebd0 --- /dev/null +++ b/tests/timeline_retrieval/locust_background_traffic.py @@ -0,0 +1,93 @@ + +#!/usr/bin/env python3 +""" +Simple background traffic for the social graph services. + +Behavior: +- Each simulated user picks a random user id (from DynamoDB if available, + else from a numeric range) and repeatedly performs POST /api/posts and + GET /api/timeline for that user. +- This test is intended as low-cost background traffic. It does not collect + or persist any custom metrics beyond what Locust itself reports. + +Run: + locust -f tests/timeline_retrieval/locust_background_traffic.py --host http://your-alb +""" + +import os +import random +import time +import logging +from typing import List + +from locust import HttpUser, task, between + +import boto3 + +logger = logging.getLogger("locust_background_traffic") +logging.basicConfig(level=logging.INFO) + + +def get_users_from_dynamodb(region: str = "us-west-2", followers_table: str = "social-graph-followers", sample_limit: int = 1000) -> List[int]: + """Try to scan the followers table and return a list of user_ids. + Falls back to an empty list on any failure. + """ + try: + ddb = boto3.resource("dynamodb", region_name=region) + table = ddb.Table(followers_table) + users = [] + resp = table.scan(Limit=sample_limit) + items = resp.get("Items", []) + users.extend([int(it["user_id"]) for it in items if "user_id" in it]) + return users + except Exception as e: + logger.debug(f"DynamoDB scan failed: {e}") + return [] + + +class BackgroundUser(HttpUser): + """Locust user that generates background post + timeline traffic.""" + + wait_time = between(0.5, 2) + + def on_start(self): + # Try to get candidate user ids from DynamoDB, else use numeric range. + region = os.environ.get("AWS_REGION", "us-west-2") + followers_table = os.environ.get("FOLLOWERS_TABLE", "social-graph-followers") + users = get_users_from_dynamodb(region=region, followers_table=followers_table, sample_limit=2000) + + if users: + self.user_pool = users + else: + # Fallback range; configurable via env TOTAL_USERS + total = int(os.environ.get("TOTAL_USERS", "10000")) + # sample a subset for better randomness without huge range operations + self.user_pool = list(range(1, min(total, 100000) + 1)) + + # pick current user id for this simulated user + self.user_id = random.choice(self.user_pool) + logger.debug(f"BackgroundUser started with user_id={self.user_id}") + + @task(8) + def read_timeline(self): + try: + # GET /api/timeline/:user_id is used elsewhere; include query fallback + path = f"/api/timeline/{self.user_id}" + with self.client.get(path, name="GET /api/timeline", timeout=5, catch_response=True) as r: + # do not process response; let Locust record default metrics + if r.status_code >= 400: + r.failure(f"status {r.status_code}") + except Exception: + # swallow errors to keep background traffic running + logger.debug("Exception during read_timeline", exc_info=True) + + @task(2) + def create_post(self): + try: + payload = {"user_id": self.user_id, "content": f"bg post {int(time.time())} from {self.user_id}"} + with self.client.post("/api/posts", json=payload, name="POST /api/posts", timeout=5, catch_response=True) as r: + if r.status_code >= 400: + r.failure(f"status {r.status_code}") + except Exception: + logger.debug("Exception during create_post", exc_info=True) + diff --git a/tests/timeline_retrieval/locust_push_fanout_test.py b/tests/timeline_retrieval/locust_fanout_test.py similarity index 100% rename from tests/timeline_retrieval/locust_push_fanout_test.py rename to tests/timeline_retrieval/locust_fanout_test.py diff --git a/tests/timeline_retrieval/locust_timeline_retrieve.py b/tests/timeline_retrieval/locust_timeline_retrieve.py new file mode 100644 index 0000000..b649aff --- /dev/null +++ b/tests/timeline_retrieval/locust_timeline_retrieve.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Locust test that seeds posts for a single selected user (using +functions from `seed_followings_posts.py`) and then only runs timeline +retrieval for that user. + +Run with: + locust -f tests/timeline_retrieval/locust_one_user.py + +By default this will pick the user nearest to 10 followings (the "eq10" +target). Override the target via the environment variable +`TARGET_USER` with values `eq10`, `medium`, or `max`. +""" + +import os +import logging +from locust import HttpUser, task, between + +# Import helper functions from the seeding script +# Use relative import when running from the same directory +try: + from . import seed_followings_posts as sfs +except ImportError: + # Fallback for when running as a script + import sys + import os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) + from tests.timeline_retrieval import seed_followings_posts as sfs + +LOG = logging.getLogger("locust_one_user") + + +class TimelineUser(HttpUser): + """Locust user that repeatedly requests the timeline for a single user. + + on_start: selects target user, seeds posts for that user's followings + using `sfs.seed_for_target`, then the Locust task repeatedly calls the + timeline endpoint for that user. + """ + + wait_time = between(1, 3) + + def on_start(self): + max_user, user_eq_10, user_medium = sfs.select_target_users() + + # Select target user based on TARGET_USER environment variable + target = os.environ.get("TARGET_USER", "eq10").lower() + if target == "max": + self.target_uid = max_user + LOG.info(f"Selected target user: {max_user} (max followings)") + elif target == "medium": + self.target_uid = user_medium + LOG.info(f"Selected target user: {user_medium} (medium followings)") + else: # default to eq10 + self.target_uid = user_eq_10 + LOG.info(f"Selected target user: {user_eq_10} (eq10 followings)") + + @task + def get_timeline(self): + # Use path parameter - route is /api/timeline/:user_id + url = f"/api/timeline/{self.target_uid}" + with self.client.get(url, name="/api/timeline", timeout=10, catch_response=True) as r: + # Check for 200; mark failures for Locust reporting + if r.status_code != 200: + error_msg = f"Status {r.status_code}" + try: + error_data = r.json() + if "error" in error_data: + error_msg = f"Status {r.status_code}: {error_data['error']}" + except: + pass + r.failure(error_msg) + LOG.error(f"Timeline request failed for user {self.target_uid}: {error_msg}") + else: + r.success() diff --git a/tests/timeline_retrieval/requirements.txt b/tests/timeline_retrieval/requirements.txt index b12ebf7..1319eee 100644 --- a/tests/timeline_retrieval/requirements.txt +++ b/tests/timeline_retrieval/requirements.txt @@ -15,7 +15,10 @@ jsonschema==4.20.0 # Logging colorlog==6.8.0 -numpy==1.26.4 +# Data processing and visualization (needed for seed_followings_posts.py) +# numpy 2.1.0+ supports Python 3.13 +numpy>=2.1.0 -matplotlib==3.9.3 +# matplotlib compatible with numpy 2.x +matplotlib>=3.9.0 diff --git a/tests/timeline_retrieval/run_locust_background.sh b/tests/timeline_retrieval/run_locust_background.sh new file mode 100644 index 0000000..a663c80 --- /dev/null +++ b/tests/timeline_retrieval/run_locust_background.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple runner for locust_push_fanout_test.py placed inside tests/timeline_retrieval +# Usage: tests/timeline_retrieval/run_locust_push_fanout.sh [mode] [--users N] [--spawn-rate N] [--run-time 5m] [--master-host HOST] [--host URL] +# Modes: ui (default), headless, master, worker + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LOCUST_FILE="tests/timeline_retrieval/locust_background_traffic.py" + +# Activate venv if present +if [ -f "$REPO_ROOT/.venv/bin/activate" ]; then + # shellcheck disable=SC1090 + source "$REPO_ROOT/.venv/bin/activate" +fi + +# Install requirements before running locust +REQUIREMENTS_FILE="$(dirname "$0")/requirements.txt" +if [ -f "$REQUIREMENTS_FILE" ]; then + echo "📦 Installing requirements from $REQUIREMENTS_FILE..." + pip install -q -r "$REQUIREMENTS_FILE" || { + echo "❌ Failed to install requirements" + exit 1 + } + echo "✅ Requirements installed" +fi + +get_alb_from_terraform() { + if [ -n "${HOST_URL:-}" ]; then + echo "$HOST_URL" + return 0 + fi + if [ -n "${ALB_URL:-}" ]; then + echo "$ALB_URL" + return 0 + fi + + TF_DIR="$REPO_ROOT/terraform" + if [ -d "$TF_DIR" ]; then + if command -v terraform >/dev/null 2>&1; then + set +e + OUT=$(cd "$TF_DIR" && terraform output -raw alb_dns_name 2>/dev/null) || OUT="" + set -e + if [ -n "$OUT" ]; then + echo "http://$OUT" + return 0 + fi + fi + fi +} + +ALB_URL_RESOLVED=$(get_alb_from_terraform) +echo "Using ALB URL: $ALB_URL_RESOLVED" + +exec locust -f locust_background_traffic.py \ + --headless \ + --users 100 \ + --spawn-rate 50 \ + --run-time 15m \ + --host "$ALB_URL_RESOLVED" diff --git a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh new file mode 100644 index 0000000..71664a8 --- /dev/null +++ b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runner for locust_timeline_retrieve.py +# Usage: +# tests/timeline_retrieval/run_locust_timeline_retrieve.sh [--users N] [--spawn-rate N] [--run-time 5m] [--target eq10|medium|max] [--report NAME] +# +# Arguments: +# --users N: Number of concurrent users (default: 1) +# --spawn-rate N: Users to spawn per second (default: 1) +# --run-time TIME: Test duration, e.g., "5m", "1h" (default: 1m) +# --target TYPE: Target user type - eq10, medium, or max (default: eq10) +# --report NAME: Report file name prefix (default: timeline_retrieve__) +# +# Environment variables: +# TARGET_USER: Target user type (eq10, medium, max) - default: eq10 +# ALB_URL or HOST_URL: Override ALB URL +# +# Output: +# Generates HTML report: .html + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LOCUST_FILE="tests/timeline_retrieval/locust_timeline_retrieve.py" + +# Activate venv if present +if [ -f "$REPO_ROOT/.venv/bin/activate" ]; then + # shellcheck disable=SC1090 + source "$REPO_ROOT/.venv/bin/activate" +fi + +# Install requirements before running locust +REQUIREMENTS_FILE="$(dirname "$0")/requirements.txt" +if [ -f "$REQUIREMENTS_FILE" ]; then + echo "📦 Installing requirements from $REQUIREMENTS_FILE..." + pip install -q -r "$REQUIREMENTS_FILE" || { + echo "❌ Failed to install requirements" + exit 1 + } + echo "✅ Requirements installed" +fi + +get_alb_from_terraform() { + if [ -n "${HOST_URL:-}" ]; then + echo "$HOST_URL" + return 0 + fi + if [ -n "${ALB_URL:-}" ]; then + echo "$ALB_URL" + return 0 + fi + + TF_DIR="$REPO_ROOT/terraform" + if [ -d "$TF_DIR" ]; then + if command -v terraform >/dev/null 2>&1; then + set +e + OUT=$(cd "$TF_DIR" && terraform output -raw alb_dns_name 2>/dev/null) || OUT="" + set -e + if [ -n "$OUT" ]; then + echo "http://$OUT" + return 0 + fi + fi + fi +} + +# Parse command line arguments +TARGET_USER="${TARGET_USER:-eq10}" +USERS=1 +SPAWN_RATE=1 +RUN_TIME="1m" +REPORT_NAME="Report_10_Followings" + +while [[ $# -gt 0 ]]; do + case $1 in + --target) + TARGET_USER="$2" + shift 2 + ;; + --users) + USERS="$2" + shift 2 + ;; + --spawn-rate) + SPAWN_RATE="$2" + shift 2 + ;; + --run-time) + RUN_TIME="$2" + shift 2 + ;; + --report) + REPORT_NAME="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Generate default report name if not specified +if [ -z "$REPORT_NAME" ]; then + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + REPORT_NAME="timeline_retrieve_${TARGET_USER}_${TIMESTAMP}" +fi + +REPORT_HTML="${REPORT_NAME}.html" + +ALB_URL_RESOLVED=$(get_alb_from_terraform) +echo "Using ALB URL: $ALB_URL_RESOLVED" +echo "Target user type: $TARGET_USER (eq10, medium, or max)" +echo "Users: $USERS, Spawn rate: $SPAWN_RATE, Run time: $RUN_TIME" +echo "Report file: $REPORT_HTML" + +# Export TARGET_USER for the Python script +export TARGET_USER + +# Set PYTHONPATH to include project root for imports +export PYTHONPATH="$REPO_ROOT:${PYTHONPATH:-}" + +# Change to the script directory +cd "$(dirname "$0")" + +# Build locust command +exec locust -f locust_timeline_retrieve.py \ + --headless \ + --users "$USERS" \ + --spawn-rate "$SPAWN_RATE" \ + --run-time "$RUN_TIME" \ + --host "$ALB_URL_RESOLVED" \ + --html "$REPORT_HTML" + diff --git a/tests/timeline_retrieval/seed_followings_posts.py b/tests/timeline_retrieval/seed_followings_posts.py index e845fdb..764015b 100644 --- a/tests/timeline_retrieval/seed_followings_posts.py +++ b/tests/timeline_retrieval/seed_followings_posts.py @@ -117,7 +117,22 @@ def scan_table(table) -> List[dict]: return items -def select_target_users(items: List[dict]) -> Tuple[int, int, int]: +def select_target_users() -> Tuple[int, int, int]: + """Scan the following table and select three target users. + + This function now performs its own DynamoDB scan (no arguments required) + and returns (max_user, user_eq_10, user_medium, items). + """ + # Ensure ALB / base url is present (keeps behavior consistent with main) + base_url = get_alb_url_from_terraform() + if not base_url: + raise RuntimeError("ALB URL could not be found from Terraform output or ALB_URL env var") + + dynamodb = boto3.resource("dynamodb", region_name="us-west-2") + table = dynamodb.Table("social-graph-following") + + items = scan_table(table) + user_following_counts = [] # (user_id, following_count) for it in items: uid_raw = it.get("user_id") @@ -164,6 +179,7 @@ def select_target_users(items: List[dict]) -> Tuple[int, int, int]: mid_idx = len(user_following_counts) // 2 user_medium = user_following_counts[mid_idx][0] user_medium_count = user_following_counts[mid_idx][1] + print(f"Selected users: max_user={max_user} ({max_count} followings), user_eq_10={user_eq_10} ({user_eq_10_count} followings), user_medium={user_medium} ({user_medium_count} followings)") return max_user, user_eq_10, user_medium @@ -245,10 +261,8 @@ def prepare_three_targets(region: str = "us-west-2", following_table_name: str = table = dynamodb.Table(following_table_name) logger.info("Scanning following table (this may take a while)...") - items = scan_table(table) - logger.info(f"Scanned {len(items)} items from {following_table_name}") - - max_user, user_eq_10, user_medium = select_target_users(items) + # select_target_users will perform its own scan and return items + max_user, user_eq_10, user_medium, items = select_target_users(region, following_table_name) logger.info(f"Selected users: max={max_user}, eq10={user_eq_10}, medium={user_medium}") # Trim eq10 to 10 and medium to 100 @@ -326,10 +340,11 @@ def main(): table = dynamodb.Table(following_table_name) logger.info("Scanning following table (this may take a while)...") - items = scan_table(table) - logger.info(f"Scanned {len(items)} items from {following_table_name}") + # select_target_users will perform its own scan and return items + max_user, user_eq_10, user_medium, items = select_target_users(region, following_table_name) + logger.info(f"Selected users: max={max_user}, eq10={user_eq_10}, medium={user_medium}") - # Draw and save following distribution plot + # Draw and save following distribution plot (items returned from select_target_users) try: plot_path = plot_following_distribution(items) if plot_path: @@ -337,9 +352,6 @@ def main(): except Exception as e: logger.debug(f"Plotting failed: {e}") - max_user, user_eq_10, user_medium = select_target_users(items) - logger.info(f"Selected users: max={max_user}, eq10={user_eq_10}, medium={user_medium}") - # Safety check # Ensure user_eq_10 has at most 10 followings in the DB; if not, trim it. try: From 1ec5a367fdde32fc3374d80e583dc1cf021fe4ee Mon Sep 17 00:00:00 2001 From: PCBZ Date: Wed, 19 Nov 2025 13:03:45 -0800 Subject: [PATCH 03/11] add bash to run --- ...ort_100_Users_10_Followings_Pull_Mode.html | 616 ++++++++++++++++++ .../Report_10_Followings.html | 58 +- ...port_10_Users_10_Followings_Pull_Mode.html | 592 +++++++++++++++++ ...eport_1_Users_10_Followings_Pull_Mode.html | 592 +++++++++++++++++ .../locust_background_traffic.py | 60 +- .../run_locust_timeline_retrieve.sh | 8 +- 6 files changed, 1880 insertions(+), 46 deletions(-) create mode 100644 tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html create mode 100644 tests/timeline_retrieval/Report_10_Users_10_Followings_Pull_Mode.html create mode 100644 tests/timeline_retrieval/Report_1_Users_10_Followings_Pull_Mode.html diff --git a/tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html b/tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html new file mode 100644 index 0000000..4f0a8ee --- /dev/null +++ b/tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html @@ -0,0 +1,616 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-19 07:07:39 - 2025-11-19 07:27:38

+

Target Host: http://cs6650-project-dev-alb-1624724957.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline4252376796031258190918423.53.1
Aggregated4252376796031258190918423.53.1
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline1000010000100001000010000110001300019000
Aggregated1000010000100001000010000110001300019000
+
+ + +
+

Failures Statistics

+ + + + + + + + + + + + + + + + + + + +
MethodNameErrorOccurrences
GET/api/timelineStatus 03767
+
+ + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_10_Followings.html b/tests/timeline_retrieval/Report_10_Followings.html index e4b626d..b8e5916 100644 --- a/tests/timeline_retrieval/Report_10_Followings.html +++ b/tests/timeline_retrieval/Report_10_Followings.html @@ -120,7 +120,7 @@

Locust Test Report

-

During: 2025-11-19 05:50:52 - 2025-11-19 05:51:50

+

During: 2025-11-19 06:12:47 - 2025-11-19 06:13:44

Target Host: http://cs6650-project-dev-alb-1624724957.us-west-2.elb.amazonaws.com

Script: locust_timeline_retrieve.py

@@ -147,12 +147,12 @@

Request Statistics

GET /api/timeline - 19 + 16 0 - 748 - 493 - 1677 - 3465 + 780 + 590 + 1010 + 7388 0.3 0.0 @@ -160,12 +160,12 @@

Request Statistics

Aggregated - 19 + 16 0 - 748 - 493 - 1677 - 3465 + 780 + 590 + 1010 + 7388 0.3 0.0 @@ -196,27 +196,27 @@

Response Time Statistics

GET /api/timeline - 710 - 800 - 820 - 890 + 790 + 790 + 860 + 870 + 970 + 1000 + 1000 1000 - 1700 - 1700 - 1700 Aggregated - 710 - 800 - 820 - 890 + 790 + 790 + 860 + 870 + 970 + 1000 + 1000 1000 - 1700 - 1700 - 1700 @@ -517,12 +517,12 @@

Final ratio

+ + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_1_Users_10_Followings_Pull_Mode.html b/tests/timeline_retrieval/Report_1_Users_10_Followings_Pull_Mode.html new file mode 100644 index 0000000..69bb809 --- /dev/null +++ b/tests/timeline_retrieval/Report_1_Users_10_Followings_Pull_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-19 06:18:23 - 2025-11-19 06:19:21

+

Target Host: http://cs6650-project-dev-alb-1624724957.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline200628496108073880.30.0
Aggregated200628496108073880.30.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline600660680720760110011001100
Aggregated600660680720760110011001100
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/locust_background_traffic.py b/tests/timeline_retrieval/locust_background_traffic.py index cc9ebd0..8a0c35f 100644 --- a/tests/timeline_retrieval/locust_background_traffic.py +++ b/tests/timeline_retrieval/locust_background_traffic.py @@ -71,23 +71,57 @@ def on_start(self): @task(8) def read_timeline(self): try: - # GET /api/timeline/:user_id is used elsewhere; include query fallback + # GET /api/timeline/:user_id + # Increased timeout to 30s for pull mode (needs to query multiple services) path = f"/api/timeline/{self.user_id}" - with self.client.get(path, name="GET /api/timeline", timeout=5, catch_response=True) as r: - # do not process response; let Locust record default metrics - if r.status_code >= 400: - r.failure(f"status {r.status_code}") - except Exception: - # swallow errors to keep background traffic running - logger.debug("Exception during read_timeline", exc_info=True) + start_time = time.time() + with self.client.get(path, name="GET /api/timeline", timeout=30, catch_response=True) as r: + elapsed = (time.time() - start_time) * 1000 # Convert to ms + + # Log slow requests for debugging + if elapsed > 1000: + logger.warning(f"Slow timeline request: user_id={self.user_id}, time={elapsed:.2f}ms, status={r.status_code}") + + if r.status_code == 200: + r.success() + elif r.status_code >= 400: + error_msg = f"status {r.status_code}" + try: + error_data = r.json() + if "error" in error_data: + error_msg = f"status {r.status_code}: {error_data['error']}" + except: + pass + r.failure(error_msg) + logger.error(f"Timeline request failed: user_id={self.user_id}, {error_msg}") + except Exception as e: + # Log exceptions for debugging + logger.error(f"Exception during read_timeline for user {self.user_id}: {e}", exc_info=True) @task(2) def create_post(self): try: payload = {"user_id": self.user_id, "content": f"bg post {int(time.time())} from {self.user_id}"} - with self.client.post("/api/posts", json=payload, name="POST /api/posts", timeout=5, catch_response=True) as r: - if r.status_code >= 400: - r.failure(f"status {r.status_code}") - except Exception: - logger.debug("Exception during create_post", exc_info=True) + start_time = time.time() + with self.client.post("/api/posts", json=payload, name="POST /api/posts", timeout=10, catch_response=True) as r: + elapsed = (time.time() - start_time) * 1000 # Convert to ms + + # Log slow requests for debugging + if elapsed > 500: + logger.warning(f"Slow post creation: user_id={self.user_id}, time={elapsed:.2f}ms, status={r.status_code}") + + if r.status_code == 200: + r.success() + elif r.status_code >= 400: + error_msg = f"status {r.status_code}" + try: + error_data = r.json() + if "error" in error_data: + error_msg = f"status {r.status_code}: {error_data['error']}" + except: + pass + r.failure(error_msg) + logger.error(f"Post creation failed: user_id={self.user_id}, {error_msg}") + except Exception as e: + logger.error(f"Exception during create_post for user {self.user_id}: {e}", exc_info=True) diff --git a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh index 71664a8..b086cb8 100644 --- a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh +++ b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh @@ -65,10 +65,10 @@ get_alb_from_terraform() { # Parse command line arguments TARGET_USER="${TARGET_USER:-eq10}" -USERS=1 -SPAWN_RATE=1 -RUN_TIME="1m" -REPORT_NAME="Report_10_Followings" +USERS=100 +SPAWN_RATE=50 +RUN_TIME="20m" +REPORT_NAME="Report_100_Users_10_Followings_Pull_Mode" while [[ $# -gt 0 ]]; do case $1 in From 87ef15bb1eb3f0a0d06f40989cbf291f09582fad Mon Sep 17 00:00:00 2001 From: PCBZ Date: Thu, 20 Nov 2025 18:23:56 -0800 Subject: [PATCH 04/11] add timeline push test result --- ...ort_100_Users_10_Followings_Pull_Mode.html | 616 ------------------ ...port_1_User_100_Followings_Push_Mode.html} | 72 +- ...eport_1_User_10_Followings_Push_Mode.html} | 72 +- ...port_1_User_500_Followings_Push_Mode.html} | 68 +- .../following_distribution.png | Bin 20835 -> 20359 bytes .../locust_background_traffic.py | 12 +- .../locust_timeline_retrieve.py | 75 +-- .../run_locust_timeline_retrieve.sh | 133 ++-- .../seed_followings_posts.py | 20 +- 9 files changed, 233 insertions(+), 835 deletions(-) delete mode 100644 tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html rename tests/timeline_retrieval/{Report_10_Followings.html => Report_1_User_100_Followings_Push_Mode.html} (98%) rename tests/timeline_retrieval/{Report_1_Users_10_Followings_Pull_Mode.html => Report_1_User_10_Followings_Push_Mode.html} (98%) rename tests/timeline_retrieval/{Report_10_Users_10_Followings_Pull_Mode.html => Report_1_User_500_Followings_Push_Mode.html} (95%) diff --git a/tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html b/tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html deleted file mode 100644 index 4f0a8ee..0000000 --- a/tests/timeline_retrieval/Report_100_Users_10_Followings_Pull_Mode.html +++ /dev/null @@ -1,616 +0,0 @@ - - - - Test Report for locust_timeline_retrieve.py - - - - -
-

Locust Test Report

- -
- -

During: 2025-11-19 07:07:39 - 2025-11-19 07:27:38

-

Target Host: http://cs6650-project-dev-alb-1624724957.us-west-2.elb.amazonaws.com

-

Script: locust_timeline_retrieve.py

-
- -
-

Request Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline4252376796031258190918423.53.1
Aggregated4252376796031258190918423.53.1
-
- -
-

Response Time Statistics

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline1000010000100001000010000110001300019000
Aggregated1000010000100001000010000110001300019000
-
- - -
-

Failures Statistics

- - - - - - - - - - - - - - - - - - - -
MethodNameErrorOccurrences
GET/api/timelineStatus 03767
-
- - - - - -
-

Charts

-
- - -
-

Final ratio

-
-
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_10_Followings.html b/tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html similarity index 98% rename from tests/timeline_retrieval/Report_10_Followings.html rename to tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html index b8e5916..143c934 100644 --- a/tests/timeline_retrieval/Report_10_Followings.html +++ b/tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html @@ -120,8 +120,8 @@

Locust Test Report

-

During: 2025-11-19 06:12:47 - 2025-11-19 06:13:44

-

Target Host: http://cs6650-project-dev-alb-1624724957.us-west-2.elb.amazonaws.com

+

During: 2025-11-20 21:58:04 - 2025-11-20 22:06:01

+

Target Host: http://cs6650-project-dev-alb-213594845.us-west-2.elb.amazonaws.com

Script: locust_timeline_retrieve.py

@@ -147,26 +147,26 @@

Request Statistics

GET /api/timeline - 16 + 197 0 - 780 - 590 - 1010 - 7388 - 0.3 + 389 + 303 + 921 + 7940 + 0.4 0.0 Aggregated - 16 + 197 0 - 780 - 590 - 1010 - 7388 - 0.3 + 389 + 303 + 921 + 7940 + 0.4 0.0 @@ -196,27 +196,27 @@

Response Time Statistics

GET /api/timeline - 790 - 790 - 860 - 870 - 970 - 1000 - 1000 - 1000 + 360 + 380 + 400 + 420 + 470 + 550 + 740 + 920 Aggregated - 790 - 790 - 860 - 870 - 970 - 1000 - 1000 - 1000 + 360 + 380 + 400 + 420 + 470 + 550 + 740 + 920 @@ -517,12 +517,12 @@

Final ratio

+ + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html b/tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html index 143c934..8bc0931 100644 --- a/tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html +++ b/tests/timeline_retrieval/Report_1_User_100_Followings_Push_Mode.html @@ -120,8 +120,8 @@

Locust Test Report

-

During: 2025-11-20 21:58:04 - 2025-11-20 22:06:01

-

Target Host: http://cs6650-project-dev-alb-213594845.us-west-2.elb.amazonaws.com

+

During: 2025-11-21 05:38:05 - 2025-11-21 05:46:03

+

Target Host: http://cs6650-project-dev-alb-515676591.us-west-2.elb.amazonaws.com

Script: locust_timeline_retrieve.py

@@ -147,26 +147,26 @@

Request Statistics

GET /api/timeline - 197 + 226 0 - 389 - 303 - 921 - 7940 - 0.4 + 65 + 29 + 3258 + 10183 + 0.5 0.0 Aggregated - 197 + 226 0 - 389 - 303 - 921 - 7940 - 0.4 + 65 + 29 + 3258 + 10183 + 0.5 0.0 @@ -196,27 +196,27 @@

Response Time Statistics

GET /api/timeline - 360 - 380 - 400 - 420 - 470 - 550 - 740 - 920 + 43 + 46 + 49 + 55 + 66 + 80 + 410 + 3300 Aggregated - 360 - 380 - 400 - 420 - 470 - 550 - 740 - 920 + 43 + 46 + 49 + 55 + 66 + 80 + 410 + 3300 @@ -517,12 +517,12 @@

Final ratio

+ + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_1_User_10_Followings_Push_Mode.html b/tests/timeline_retrieval/Report_1_User_10_Followings_Push_Mode.html index 4b61e65..29bc176 100644 --- a/tests/timeline_retrieval/Report_1_User_10_Followings_Push_Mode.html +++ b/tests/timeline_retrieval/Report_1_User_10_Followings_Push_Mode.html @@ -120,8 +120,8 @@

Locust Test Report

-

During: 2025-11-20 20:50:06 - 2025-11-20 20:58:05

-

Target Host: http://cs6650-project-dev-alb-213594845.us-west-2.elb.amazonaws.com

+

During: 2025-11-21 06:34:42 - 2025-11-21 06:35:51

+

Target Host: http://cs6650-project-dev-alb-515676591.us-west-2.elb.amazonaws.com

Script: locust_timeline_retrieve.py

@@ -147,26 +147,26 @@

Request Statistics

GET /api/timeline - 227 - 0 - 94 - 61 - 673 - 7930 - 0.5 + 21 + 1 + 1557 + 36 + 30014 + 8577 + 0.3 0.0 Aggregated - 227 - 0 - 94 - 61 - 673 - 7930 - 0.5 + 21 + 1 + 1557 + 36 + 30014 + 8577 + 0.3 0.0 @@ -196,27 +196,27 @@

Response Time Statistics

GET /api/timeline - 81 - 86 - 92 - 100 - 120 - 160 - 320 - 670 + 57 + 73 + 110 + 200 + 410 + 750 + 30000 + 30000 Aggregated - 81 - 86 - 92 - 100 - 120 - 160 - 320 - 670 + 57 + 73 + 110 + 200 + 410 + 750 + 30000 + 30000 @@ -224,6 +224,30 @@

Response Time Statistics

+
+

Failures Statistics

+ + + + + + + + + + + + + + + + + + + +
MethodNameErrorOccurrences
GET/api/timelineTimeline error (status 0): HTTP 01
+
+ @@ -517,12 +541,12 @@

Final ratio

+ + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_1_User_500_Followings_Push_Mode.html b/tests/timeline_retrieval/Report_1_User_500_Followings_Push_Mode.html index 5d74ee9..e641b8b 100644 --- a/tests/timeline_retrieval/Report_1_User_500_Followings_Push_Mode.html +++ b/tests/timeline_retrieval/Report_1_User_500_Followings_Push_Mode.html @@ -120,8 +120,8 @@

Locust Test Report

-

During: 2025-11-20 21:10:26 - 2025-11-20 21:18:23

-

Target Host: http://cs6650-project-dev-alb-213594845.us-west-2.elb.amazonaws.com

+

During: 2025-11-21 05:48:32 - 2025-11-21 05:56:30

+

Target Host: http://cs6650-project-dev-alb-515676591.us-west-2.elb.amazonaws.com

Script: locust_timeline_retrieve.py

@@ -147,26 +147,26 @@

Request Statistics

GET /api/timeline - 124 + 230 0 - 1843 - 1399 - 16285 - 7941 - 0.3 + 56 + 31 + 309 + 10225 + 0.5 0.0 Aggregated - 124 + 230 0 - 1843 - 1399 - 16285 - 7941 - 0.3 + 56 + 31 + 309 + 10225 + 0.5 0.0 @@ -196,27 +196,27 @@

Response Time Statistics

GET /api/timeline - 1600 - 1700 - 1700 - 1900 - 2200 - 2500 - 3300 - 16000 + 46 + 48 + 53 + 58 + 86 + 120 + 230 + 310 Aggregated - 1600 - 1700 - 1700 - 1900 - 2200 - 2500 - 3300 - 16000 + 46 + 48 + 53 + 58 + 86 + 120 + 230 + 310 @@ -517,12 +517,12 @@

Final ratio

+ + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_25K_1_User_10_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_25K_1_User_10_Followings_Hybrid_Mode.html new file mode 100644 index 0000000..3b3af5e --- /dev/null +++ b/tests/timeline_retrieval/Report_25K_1_User_10_Followings_Hybrid_Mode.html @@ -0,0 +1,616 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-23 23:41:42 - 2025-11-23 23:46:41

+

Target Host: http://cs6650-project-dev-alb-111554498.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline1471563726778160.50.0
Aggregated1471563726778160.50.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline525458607178160270
Aggregated525458607178160270
+
+ + +
+

Failures Statistics

+ + + + + + + + + + + + + + + + + + + +
MethodNameErrorOccurrences
GET/api/timelineTimeline error (status 500): failed to get posts from Post Service: failed to call post service: rpc error: code = Unavailable desc = upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 1111
+
+ + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_25K_1_User_10_Followings_Pull_Mode.html b/tests/timeline_retrieval/Report_25K_1_User_10_Followings_Pull_Mode.html new file mode 100644 index 0000000..284949d --- /dev/null +++ b/tests/timeline_retrieval/Report_25K_1_User_10_Followings_Pull_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-23 23:10:25 - 2025-11-23 23:15:24

+

Target Host: http://cs6650-project-dev-alb-111554498.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline1420573849578680.50.0
Aggregated1420573849578680.50.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline505156627191140500
Aggregated505156627191140500
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Pull_Mode.html b/tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Pull_Mode.html new file mode 100644 index 0000000..43ca432 --- /dev/null +++ b/tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Pull_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-23 23:31:06 - 2025-11-23 23:35:59

+

Target Host: http://cs6650-project-dev-alb-111554498.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline57032292789405080390.20.0
Aggregated57032292789405080390.20.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline32003200330034003600390041004100
Aggregated32003200330034003600390041004100
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/following_distribution.png b/tests/timeline_retrieval/following_distribution.png index 1da71774056f176cef2069d9022053e9b7272a85..495c5aac32ad37b495db87ae40a5d09191e3818d 100644 GIT binary patch literal 23018 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 F{15sfs__5- diff --git a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh index 630083f..3785e07 100644 --- a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh +++ b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh @@ -99,8 +99,8 @@ PY TARGET_USER="medium" USERS=1 SPAWN_RATE=1 -RUN_TIME="8m" -REPORT_NAME="Report_1_User_100_Followings_Pull_Mode" +RUN_TIME="5m" +REPORT_NAME="Report_25K_1_User_100_Followings_Hybrid_Mode" REPORT_HTML="${REPORT_NAME}.html" ALB_URL_RESOLVED=$(get_alb_from_terraform) From 70fac594af403dd9005722c6aa7d8abe0152b037 Mon Sep 17 00:00:00 2001 From: PCBZ Date: Sun, 23 Nov 2025 22:50:15 -0800 Subject: [PATCH 10/11] add hybrid results --- ...25K_1_User_100_Followings_Hybrid_Mode.html | 592 +++++++++++++++++ ...5K_1_User_1600_Followings_Hybrid_Mode.html | 616 ++++++++++++++++++ ...t_K_1_User_500_Followings_Hybrid_Mode.html | 592 +++++++++++++++++ .../following_distribution.png | Bin 23018 -> 21117 bytes .../run_locust_timeline_retrieve.sh | 4 +- 5 files changed, 1802 insertions(+), 2 deletions(-) create mode 100644 tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html create mode 100644 tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Hybrid_Mode.html create mode 100644 tests/timeline_retrieval/Report_K_1_User_500_Followings_Hybrid_Mode.html diff --git a/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html new file mode 100644 index 0000000..5c21083 --- /dev/null +++ b/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +

+

Locust Test Report

+ +
+ +

During: 2025-11-23 23:51:04 - 2025-11-23 23:56:03

+

Target Host: http://cs6650-project-dev-alb-111554498.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline13701625984880420.50.0
Aggregated13701625984880420.50.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline150160190220230250390850
Aggregated150160190220230250390850
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Hybrid_Mode.html new file mode 100644 index 0000000..3468613 --- /dev/null +++ b/tests/timeline_retrieval/Report_25K_1_User_1600_Followings_Hybrid_Mode.html @@ -0,0 +1,616 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-24 04:00:39 - 2025-11-24 04:01:07

+

Target Host: http://cs6650-project-dev-alb-111554498.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline9910427684712900.30.3
Aggregated9910427684712900.30.3
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline981101802008500850085008500
Aggregated981101802008500850085008500
+
+ + +
+

Failures Statistics

+ + + + + + + + + + + + + + + + + + + +
MethodNameErrorOccurrences
GET/api/timelineTimeline error (status 500): failed to get following list from Social Graph Service: failed to call GetFollowingList: rpc error: code = Unavailable desc = upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 1119
+
+ + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_K_1_User_500_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_K_1_User_500_Followings_Hybrid_Mode.html new file mode 100644 index 0000000..bc89a77 --- /dev/null +++ b/tests/timeline_retrieval/Report_K_1_User_500_Followings_Hybrid_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-24 06:11:05 - 2025-11-24 06:14:19

+

Target Host: http://cs6650-project-dev-alb-1497973835.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline30044354044537880450.20.0
Aggregated30044354044537880450.20.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline44004500450046004700480054005400
Aggregated44004500450046004700480054005400
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/following_distribution.png b/tests/timeline_retrieval/following_distribution.png index 495c5aac32ad37b495db87ae40a5d09191e3818d..fb3a0f9d4ff237e8fca1e49bd846b448542a5466 100644 GIT binary patch literal 21117 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 literal 23018 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 v{0X6eweat9>OTpAf5SEZ$yM*ysN8PtZv` Date: Tue, 25 Nov 2025 16:46:42 -0800 Subject: [PATCH 11/11] add test results --- .../internal/repository/post_repository.go | 167 ++++- .../internal/service/post_service.go | 50 +- .../timeline-service/src/fanout/hybrid.go | 33 +- .../timeline-service/src/grpc/user_service.go | 83 ++- services/user-service/main.go | 44 +- .../terraform/modules/ecs/main.tf | 5 + terraform/main.tf | 4 + ...25K_1_User_100_Followings_Hybrid_Mode.html | 56 +- ..._25K_1_User_10_Followings_Hybrid_Mode.html | 82 +-- ...5K_1_User_1600_Followings_Hybrid_Mode.html | 104 ++- ..._5K_1_User_100_Followings_Hybrid_Mode.html | 592 ++++++++++++++++++ ...t_5K_1_User_10_Followings_Hybrid_Mode.html | 592 ++++++++++++++++++ ..._5K_1_User_500_Followings_Hybrid_Mode.html | 592 ++++++++++++++++++ .../following_distribution.png | Bin 21117 -> 20484 bytes .../run_locust_timeline_retrieve.sh | 2 +- 15 files changed, 2171 insertions(+), 235 deletions(-) create mode 100644 tests/timeline_retrieval/Report_5K_1_User_100_Followings_Hybrid_Mode.html create mode 100644 tests/timeline_retrieval/Report_5K_1_User_10_Followings_Hybrid_Mode.html create mode 100644 tests/timeline_retrieval/Report_5K_1_User_500_Followings_Hybrid_Mode.html diff --git a/services/post-service/internal/repository/post_repository.go b/services/post-service/internal/repository/post_repository.go index b2f2996..9e45242 100644 --- a/services/post-service/internal/repository/post_repository.go +++ b/services/post-service/internal/repository/post_repository.go @@ -3,8 +3,11 @@ package repository import ( "context" "fmt" + "log" + "os" "strconv" "sync" + "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" @@ -81,42 +84,147 @@ func (r *PostRepository) GetPost(ctx context.Context, postID int64) (*pb.Post, e return &post, err } +// batchCheckUsersHasPosts performs parallel COUNT queries to check which users have posts +func (r *PostRepository) batchCheckUsersHasPosts(ctx context.Context, userIDs []int64) (map[int64]bool, error) { + if len(userIDs) == 0 { + return make(map[int64]bool), nil + } + + hasPostsMap := make(map[int64]bool, len(userIDs)) + hasPostsMutex := &sync.Mutex{} + maxWorkers := min(50, len(userIDs)) + + // Create worker pool for COUNT queries + userIDChan := make(chan int64, len(userIDs)) + for _, userID := range userIDs { + userIDChan <- userID + } + close(userIDChan) + + var wg sync.WaitGroup + errChan := make(chan error, len(userIDs)) + + // Launch worker pool for parallel COUNT queries + for i := 0; i < maxWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for userID := range userIDChan { + hasPosts, err := r.checkUserHasPosts(ctx, userID) + if err != nil { + errChan <- fmt.Errorf("failed to check posts for user %d: %w", userID, err) + continue + } + + hasPostsMutex.Lock() + hasPostsMap[userID] = hasPosts + hasPostsMutex.Unlock() + } + }() + } + + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, err + } + } + + return hasPostsMap, nil +} + // Retrieve recent posts for multiple users (parallel execution with worker pool for better performance) func (r *PostRepository) GetPostByUserIDs(ctx context.Context, userIDs []int64, limit int32) (map[int64][]*pb.Post, error) { - result := make(map[int64][]*pb.Post) + // Check if we're in hybrid mode (read from environment variable) + postStrategy := os.Getenv("POST_STRATEGY") + checkCountFirst := postStrategy == "hybrid" + startTime := time.Now() + // Pre-allocate result map with expected capacity to reduce reallocation + result := make(map[int64][]*pb.Post, len(userIDs)) resultMutex := &sync.Mutex{} + // If in hybrid mode, first batch check which users have posts + var usersToQuery []int64 + if checkCountFirst { + countStart := time.Now() + hasPostsMap, err := r.batchCheckUsersHasPosts(ctx, userIDs) + if err != nil { + return nil, fmt.Errorf("failed to batch check users has posts: %w", err) + } + countDuration := time.Since(countStart) + + // Filter users that have posts + usersToQuery = make([]int64, 0, len(userIDs)) + for _, userID := range userIDs { + if hasPostsMap[userID] { + usersToQuery = append(usersToQuery, userID) + } else { + // User has no posts, set empty result immediately + result[userID] = []*pb.Post{} + } + } + + log.Printf("[BatchGetPosts] Batch COUNT check: users=%d, has_posts=%d, no_posts=%d, duration=%v", + len(userIDs), len(usersToQuery), len(userIDs)-len(usersToQuery), countDuration) + } else { + // Not in hybrid mode, query all users + usersToQuery = userIDs + } + + // If no users have posts, return early + if len(usersToQuery) == 0 { + totalDuration := time.Since(startTime) + log.Printf("[BatchGetPosts] Completed: users=%d, duration=%v (all users have no posts)", + len(userIDs), totalDuration) + return result, nil + } + // Limit concurrent goroutines to avoid resource exhaustion - maxWorkers := min(50, len(userIDs)) // Adjust maxWorkers as needed + maxWorkers := min(50, len(usersToQuery)) // Create worker pool using buffered channel - userIDChan := make(chan int64, len(userIDs)) - for _, userID := range userIDs { + userIDChan := make(chan int64, len(usersToQuery)) + for _, userID := range usersToQuery { userIDChan <- userID } close(userIDChan) // Use WaitGroup to wait for all workers to complete var wg sync.WaitGroup - errChan := make(chan error, len(userIDs)) + errChan := make(chan error, len(usersToQuery)) - // Launch worker pool + // Launch worker pool - now we know these users have posts, so skip COUNT check for i := 0; i < maxWorkers; i++ { wg.Add(1) go func() { defer wg.Done() for userID := range userIDChan { - posts, err := r.GetPostByUserID(ctx, userID, limit) + queryStart := time.Now() + // Skip COUNT check since we already verified these users have posts + posts, err := r.GetPostByUserID(ctx, userID, limit, false) + queryDuration := time.Since(queryStart) + if err != nil { errChan <- fmt.Errorf("failed to get posts for user %d: %w", userID, err) continue } - // Thread-safe write to result map + // Optimization: Only write to result map if posts exist or if we want to track empty results + // For hybrid mode, we may want to skip empty results to reduce map size + // But for consistency, we'll include all users (even with empty posts) resultMutex.Lock() result[userID] = posts resultMutex.Unlock() + + // Log slow queries for analysis + if queryDuration > 50*time.Millisecond { + log.Printf("[BatchGetPosts] Slow query: user_id=%d, duration=%v, posts=%d", userID, queryDuration, len(posts)) + } } }() } @@ -132,11 +240,52 @@ func (r *PostRepository) GetPostByUserIDs(ctx context.Context, userIDs []int64, } } + totalDuration := time.Since(startTime) + log.Printf("[BatchGetPosts] Completed: users=%d, duration=%v", + len(userIDs), totalDuration) + return result, nil } +// checkUserHasPosts quickly checks if a user has any posts using COUNT query +func (r *PostRepository) checkUserHasPosts(ctx context.Context, userID int64) (bool, error) { + result, err := r.client.Query(ctx, &dynamodb.QueryInput{ + TableName: aws.String(r.tableName), + IndexName: aws.String("user_id-index"), + KeyConditionExpression: aws.String("user_id = :uid"), + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":uid": &types.AttributeValueMemberN{ + Value: fmt.Sprintf("%d", userID), + }, + }, + Select: types.SelectCount, // Only return count, not data + Limit: aws.Int32(1), // Only need to know if count > 0 + }) + + if err != nil { + return false, err + } + + return result.Count > 0, nil +} + // Retrieve recent posts for single user -func (r *PostRepository) GetPostByUserID(ctx context.Context, userID int64, limit int32) ([]*pb.Post, error) { +func (r *PostRepository) GetPostByUserID(ctx context.Context, userID int64, limit int32, checkCountFirst bool) ([]*pb.Post, error) { + // Optimization for hybrid mode: First check if user has posts using COUNT query + // This avoids fetching data for users with no posts + if checkCountFirst { + hasPosts, err := r.checkUserHasPosts(ctx, userID) + if err != nil { + return nil, err + } + + if !hasPosts { + // User has no posts, return empty slice immediately + return []*pb.Post{}, nil + } + } + + // User has posts (or checkCountFirst is false), fetch the actual data result, err := r.client.Query(ctx, &dynamodb.QueryInput{ TableName: aws.String(r.tableName), IndexName: aws.String("user_id-index"), // Use GSI for querying by user_id diff --git a/services/post-service/internal/service/post_service.go b/services/post-service/internal/service/post_service.go index a5de11a..5e6e005 100644 --- a/services/post-service/internal/service/post_service.go +++ b/services/post-service/internal/service/post_service.go @@ -16,13 +16,13 @@ const ( ) type PostService struct { - repo *repository.PostRepository + repo *repository.PostRepository fanoutService *FanoutService } func NewPostService(repo *repository.PostRepository, fanoutService *FanoutService) *PostService { return &PostService{ - repo: repo, + repo: repo, fanoutService: fanoutService, } } @@ -37,47 +37,47 @@ func (s *PostService) createPost(req *model.CreatePostRequest) *pb.Post { } } -func (s *PostService)PushStrategy(ctx context.Context, req *model.CreatePostRequest) (*pb.Post, error) { +func (s *PostService) PushStrategy(ctx context.Context, req *model.CreatePostRequest) (*pb.Post, error) { post := s.createPost(req) // Fanout go func() { - if err := s.fanoutService.ExecutePushFanout(context.Background(), post); err!= nil { + if err := s.fanoutService.ExecutePushFanout(context.Background(), post); err != nil { fmt.Printf("Fan-out error for post %d: %v\n", post.PostId, err) } }() return post, nil } -func (s *PostService)PullStrategy(ctx context.Context, req *model.CreatePostRequest) (*pb.Post, error) { +func (s *PostService) PullStrategy(ctx context.Context, req *model.CreatePostRequest) (*pb.Post, error) { post := s.createPost(req) // Save to DynamoDB - if err:= s.repo.CreatePost(ctx, post); err != nil { + if err := s.repo.CreatePost(ctx, post); err != nil { return nil, fmt.Errorf("failed to create post: %w", err) } return post, nil } -func (s *PostService)HybridStrategy(ctx context.Context, req *model.CreatePostRequest, hybridThreshold int) (*pb.Post, error) { +func (s *PostService) HybridStrategy(ctx context.Context, req *model.CreatePostRequest, hybridThreshold int) (*pb.Post, error) { post := s.createPost(req) // Get follower count - followers, err := s.fanoutService.socialGraphClient.GetFollowers(ctx, post.UserId, 1, 0) - if err != nil { - return post, fmt.Errorf("failed to get followers: %w", err) - } - - log.Printf("User %d has %d followers", post.UserId, followers.TotalCount) - - // Check threshold - if followers.TotalCount >= int32(hybridThreshold) { - log.Printf("User %d has >= %d followers, skipping push fan-out", post.UserId, hybridThreshold) - post, err = s.PullStrategy(ctx,req) - if err != nil { - return nil, fmt.Errorf("failed to create post: %w", err) - } - return post, nil + followers, err := s.fanoutService.socialGraphClient.GetFollowers(ctx, post.UserId, 1, 0) + if err != nil { + return post, fmt.Errorf("failed to get followers: %w", err) + } + + log.Printf("User %d has %d followers", post.UserId, followers.TotalCount) + + // Check threshold + if followers.TotalCount >= int32(hybridThreshold) { + log.Printf("User %d has >= %d followers, skipping push fan-out", post.UserId, hybridThreshold) + post, err = s.PullStrategy(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to create post: %w", err) + } + return post, nil } post, err = s.PushStrategy(ctx, req) @@ -88,12 +88,12 @@ func (s *PostService)HybridStrategy(ctx context.Context, req *model.CreatePostRe } // Get single post -func (s *PostService)GetPost(ctx context.Context, postID int64) (*pb.Post, error) { +func (s *PostService) GetPost(ctx context.Context, postID int64) (*pb.Post, error) { return s.repo.GetPost(ctx, postID) } // BatchGetPosts for Timeline Service -func (s *PostService) BatchGetPosts(ctx context.Context, req *pb.BatchGetPostsRequest)(map[int64]*pb.PostList, error) { +func (s *PostService) BatchGetPosts(ctx context.Context, req *pb.BatchGetPostsRequest) (map[int64]*pb.PostList, error) { if req.Limit == 0 { req.Limit = PostsLimit } @@ -109,5 +109,3 @@ func (s *PostService) BatchGetPosts(ctx context.Context, req *pb.BatchGetPostsRe } return result, nil } - - diff --git a/services/timeline-service/src/fanout/hybrid.go b/services/timeline-service/src/fanout/hybrid.go index 122837e..179f15c 100644 --- a/services/timeline-service/src/fanout/hybrid.go +++ b/services/timeline-service/src/fanout/hybrid.go @@ -3,6 +3,8 @@ package fanout import ( "container/heap" "fmt" + "log" + "time" "github.com/PCBZ/CS6650-Project/services/timeline-service/src/grpc" "github.com/PCBZ/CS6650-Project/services/timeline-service/src/models" @@ -39,21 +41,26 @@ func (s *HybridStrategy) GetTimeline(userID int64, limit int) (*models.TimelineR timeline *models.TimelineResponse err error source string + duration time.Duration } pushChan := make(chan result, 1) pullChan := make(chan result, 1) - // Execute push strategy concurrently + // Execute push strategy concurrently (fetch from database) go func() { + startTime := time.Now() timeline, err := s.pushStrategy.GetTimeline(userID, limit) - pushChan <- result{timeline: timeline, err: err, source: "push"} + duration := time.Since(startTime) + pushChan <- result{timeline: timeline, err: err, source: "push", duration: duration} }() - // Execute pull strategy concurrently + // Execute pull strategy concurrently (fetch from gRPC) go func() { + startTime := time.Now() timeline, err := s.pullStrategy.GetTimeline(userID, limit) - pullChan <- result{timeline: timeline, err: err, source: "pull"} + duration := time.Since(startTime) + pullChan <- result{timeline: timeline, err: err, source: "pull", duration: duration} }() // Wait for both results @@ -65,6 +72,24 @@ func (s *HybridStrategy) GetTimeline(userID int64, limit int) (*models.TimelineR } } + // Log timing information + log.Printf("[HYBRID_TIMING] user_id=%d, database_fetch_duration=%v, grpc_fetch_duration=%v, database_posts=%d, grpc_posts=%d", + userID, + pushResult.duration, + pullResult.duration, + func() int { + if pushResult.timeline != nil { + return len(pushResult.timeline.Timeline) + } + return 0 + }(), + func() int { + if pullResult.timeline != nil { + return len(pullResult.timeline.Timeline) + } + return 0 + }()) + // Merge results - combine posts from both strategies return s.mergeTimelines(pushResult.timeline, pullResult.timeline, pushResult.err, pullResult.err, limit) } diff --git a/services/timeline-service/src/grpc/user_service.go b/services/timeline-service/src/grpc/user_service.go index 9d08fb6..1eb8126 100644 --- a/services/timeline-service/src/grpc/user_service.go +++ b/services/timeline-service/src/grpc/user_service.go @@ -32,14 +32,77 @@ type UserServiceClient interface { // userServiceClient implements UserServiceClient with actual gRPC calls type userServiceClient struct { - client pb.UserServiceClient - conn *grpc.ClientConn + client pb.UserServiceClient + conn *grpc.ClientConn + endpoint string +} + +const ( + userServiceReconnectMaxAttempts = 20 // Increased from 5 to 20 to handle slow startup + userServiceReconnectBaseDelay = 1 * time.Second // Increased from 500ms to 1s + userServiceReconnectMaxDelay = 10 * time.Second // Maximum delay between retries +) + +// ensureConnection ensures the gRPC connection is established, retrying if needed +func (c *userServiceClient) ensureConnection(ctx context.Context) error { + if c.client != nil && c.conn != nil { + // Connection already established + return nil + } + + // Try to reconnect with retries and exponential backoff + var lastErr error + for attempt := 1; attempt <= userServiceReconnectMaxAttempts; attempt++ { + log.Printf("Attempting to reconnect to User Service at %s (attempt %d/%d)...", c.endpoint, attempt, userServiceReconnectMaxAttempts) + + connCtx, cancel := context.WithTimeout(ctx, 15*time.Second) // Increased timeout from 10s to 15s + conn, err := grpc.DialContext( + connCtx, + c.endpoint, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) + cancel() + + if err == nil { + // Close previous connection if exists + if c.conn != nil { + _ = c.conn.Close() + } + + c.conn = conn + c.client = pb.NewUserServiceClient(conn) + log.Printf("Successfully reconnected to User Service at %s", c.endpoint) + return nil + } + + lastErr = err + log.Printf("Failed to reconnect to User Service (attempt %d/%d): %v", attempt, userServiceReconnectMaxAttempts, err) + + // Calculate exponential backoff delay with cap + delay := userServiceReconnectBaseDelay * time.Duration(1< userServiceReconnectMaxDelay { + delay = userServiceReconnectMaxDelay + } + log.Printf("Waiting %v before next retry...", delay) + + // Respect context cancellation + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled while reconnecting to user service: %w", ctx.Err()) + case <-time.After(delay): + // Continue to next attempt + } + } + + return fmt.Errorf("failed to reconnect to user service after %d attempts: %w", userServiceReconnectMaxAttempts, lastErr) } // BatchGetUserInfo calls the real User Service via gRPC func (c *userServiceClient) BatchGetUserInfo(ctx context.Context, userIDs []int64) (*BatchGetUserInfoResponse, error) { - if c.client == nil { - return nil, fmt.Errorf("user service client not initialized - connection failed at startup") + // Ensure connection is established, retry if needed + if err := c.ensureConnection(ctx); err != nil { + return nil, fmt.Errorf("user service client not initialized - connection failed: %w", err) } // Create gRPC request @@ -92,18 +155,20 @@ func NewUserServiceClient(endpoint string) UserServiceClient { grpc.WithBlock(), // Block until connection is established ) if err != nil { - // Return a client that will fail on first use, but allow service to start + // Return a client that will retry on first use, but allow service to start log.Printf("Warning: Failed to connect to user service at %s: %v. Service will retry on first use.", endpoint, err) return &userServiceClient{ - client: nil, - conn: nil, + client: nil, + conn: nil, + endpoint: endpoint, } } log.Printf("User Service client created for %s", endpoint) return &userServiceClient{ - client: pb.NewUserServiceClient(conn), - conn: conn, + client: pb.NewUserServiceClient(conn), + conn: conn, + endpoint: endpoint, } } diff --git a/services/user-service/main.go b/services/user-service/main.go index 78147f9..8cb8482 100644 --- a/services/user-service/main.go +++ b/services/user-service/main.go @@ -123,7 +123,7 @@ func main() { } } -// initializeServiceDatabase creates the service database and user if they don't exist +// initializeServiceDatabase creates the service database if it doesn't exist func initializeServiceDatabase(host, port, masterUser, masterPassword, sslMode, dbName string) error { // Validate database name to prevent SQL injection (alphanumeric and underscores only) dbNamePattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) @@ -166,46 +166,8 @@ func initializeServiceDatabase(host, port, masterUser, masterPassword, sslMode, log.Printf("Database %s already exists", dbName) } - // Create service user if it doesn't exist (optional - for future use) - serviceUser := fmt.Sprintf("%s_user", dbName) - // Validate service user name - if !dbNamePattern.MatchString(serviceUser) { - return fmt.Errorf("invalid service user name: must contain only alphanumeric characters and underscores") - } - - var userExists bool - checkUserQuery := "SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)" - err = masterDB.QueryRow(checkUserQuery, serviceUser).Scan(&userExists) - if err != nil { - return fmt.Errorf("failed to check if user exists: %w", err) - } - - if !userExists { - // Use a more secure approach: create user with a placeholder password, then alter it - // This prevents password exposure in query logs - createUserQuery := fmt.Sprintf("CREATE USER %s", pq.QuoteIdentifier(serviceUser)) - _, err = masterDB.Exec(createUserQuery) - if err != nil { - return fmt.Errorf("failed to create user %s: %w", serviceUser, err) - } - - // Set password in a separate statement to minimize exposure - setPasswordQuery := fmt.Sprintf("ALTER USER %s WITH PASSWORD $1", pq.QuoteIdentifier(serviceUser)) - _, err = masterDB.Exec(setPasswordQuery, masterPassword) - if err != nil { - return fmt.Errorf("failed to set password for user %s: %w", serviceUser, err) - } - - // Grant privileges to the service user - grantQuery := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(serviceUser)) - _, err = masterDB.Exec(grantQuery) - if err != nil { - return fmt.Errorf("failed to grant privileges to user %s: %w", serviceUser, err) - } - log.Printf("Created user: %s and granted privileges", serviceUser) - } else { - log.Printf("User %s already exists", serviceUser) - } + // Note: We skip user creation and use the postgres user directly + // This simplifies startup and avoids permission issues return nil } diff --git a/services/user-service/terraform/modules/ecs/main.tf b/services/user-service/terraform/modules/ecs/main.tf index 23d0f82..027a458 100644 --- a/services/user-service/terraform/modules/ecs/main.tf +++ b/services/user-service/terraform/modules/ecs/main.tf @@ -110,6 +110,11 @@ resource "aws_ecs_service" "app" { # CRITICAL: Ensure clean shutdown during destroy enable_execute_command = false wait_for_steady_state = false + + # Give user service time to initialize database connection and schema + # User service needs to: connect to RDS, check/create database, initialize schema + # This typically takes 10-30 seconds, so we set grace period to 60 seconds + health_check_grace_period_seconds = 60 network_configuration { subnets = var.subnet_ids diff --git a/terraform/main.tf b/terraform/main.tf index 3e3c948..d6e0127 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -218,6 +218,10 @@ module "timeline_service" { memory_target_value = var.timeline_service_memory_target_value enable_request_based_scaling = var.timeline_service_enable_request_based_scaling request_count_target_value = var.timeline_service_request_count_target_value + + # Ensure user-service is deployed and ready before timeline-service starts + # This helps with gRPC connection timing + depends_on = [module.user_service] } # Social Graph Service diff --git a/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html index 5c21083..68e27ae 100644 --- a/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html +++ b/tests/timeline_retrieval/Report_25K_1_User_100_Followings_Hybrid_Mode.html @@ -120,8 +120,8 @@

Locust Test Report

-

During: 2025-11-23 23:51:04 - 2025-11-23 23:56:03

-

Target Host: http://cs6650-project-dev-alb-111554498.us-west-2.elb.amazonaws.com

+

During: 2025-11-25 21:18:03 - 2025-11-25 21:23:02

+

Target Host: http://cs6650-project-dev-alb-1947481816.us-west-2.elb.amazonaws.com

Script: locust_timeline_retrieve.py

@@ -147,12 +147,12 @@

Request Statistics

GET /api/timeline - 137 + 138 0 - 162 - 59 - 848 - 8042 + 132 + 57 + 1180 + 31 0.5 0.0 @@ -160,12 +160,12 @@

Request Statistics

Aggregated - 137 + 138 0 - 162 - 59 - 848 - 8042 + 132 + 57 + 1180 + 31 0.5 0.0 @@ -196,27 +196,27 @@

Response Time Statistics

GET /api/timeline + 110 + 130 + 140 150 - 160 - 190 + 180 220 - 230 - 250 - 390 - 850 + 880 + 1200 Aggregated + 110 + 130 + 140 150 - 160 - 190 + 180 220 - 230 - 250 - 390 - 850 + 880 + 1200 @@ -517,12 +517,12 @@

Final ratio

+ + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_5K_1_User_10_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_5K_1_User_10_Followings_Hybrid_Mode.html new file mode 100644 index 0000000..d7ecfd5 --- /dev/null +++ b/tests/timeline_retrieval/Report_5K_1_User_10_Followings_Hybrid_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-25 06:31:15 - 2025-11-25 06:36:14

+

Target Host: http://cs6650-project-dev-alb-1744692205.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline138098471187102350.50.0
Aggregated138098471187102350.50.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline677686941601908701200
Aggregated677686941601908701200
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/Report_5K_1_User_500_Followings_Hybrid_Mode.html b/tests/timeline_retrieval/Report_5K_1_User_500_Followings_Hybrid_Mode.html new file mode 100644 index 0000000..b06ada3 --- /dev/null +++ b/tests/timeline_retrieval/Report_5K_1_User_500_Followings_Hybrid_Mode.html @@ -0,0 +1,592 @@ + + + + Test Report for locust_timeline_retrieve.py + + + + +
+

Locust Test Report

+ +
+ +

During: 2025-11-25 06:26:45 - 2025-11-25 06:27:15

+

Target Host: http://cs6650-project-dev-alb-1744692205.us-west-2.elb.amazonaws.com

+

Script: locust_timeline_retrieve.py

+
+ +
+

Request Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)Average size (bytes)RPSFailures/s
GET/api/timeline110863699155886660.40.0
Aggregated110863699155886660.40.0
+
+ +
+

Response Time Statistics

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
GET/api/timeline840850860860950160016001600
Aggregated840850860860950160016001600
+
+ + + + + + +
+

Charts

+
+ + +
+

Final ratio

+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/timeline_retrieval/following_distribution.png b/tests/timeline_retrieval/following_distribution.png index fb3a0f9d4ff237e8fca1e49bd846b448542a5466..c562d54f231d171b112921de7e54f3814dd2e0f5 100644 GIT binary patch literal 20484 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 literal 21117 zcmd742Ut|u)-75{n^3eB1tgjv0Em$-1WJ0bble{mrb?w^28aJp@w&xUAEW8$Ws@_-7F>x z7nBx!8VcKE8WbC}uO5rIzyuGI^JXm4&l~<{?~X?JdlKd+SNO{j3==Q>!J?wF5rYX_ z_w&E{EBzr&rKN#4Z{J=M@$tqdxL1p?uC6YWTQ&GKYe)-64J(f%-8D;aYkRmqPRY-k z$Gzi56Tzy%u8+s1RxG%L8ZYZ&_W8k%rhLb~HyIhBrSqM(DT#s;!8uw(%6y}JgBGh? zn4~xG#F5nzdL}XJ(}HRq31)DX`YeR@c=RcX6pg%$ma>QMo~@JatNn%Etfg#RaNx>7 zXXNKmJT{THit0!8ajKJS|IDVe6g~2lF~_rB;r=Iu}y? zu^S$*wZo-B@l{5Kh@9KlY(TkPXKsy14e{|F@s}>c^-V8M-?vtq=*ka!8K>aoyp{9# z^@r5rvA3z`Y}>NTt0G01Fp&>3!d)!A4m-w`F(Gjdn^ZK6xcjodL8G$rKK=+#`pUc(=Wex zeKH^BzuS9#vb&JvM=ThR(7kq)w(~~+=ZESNx?a6f%ISIcLQbyz__UpTUwP$*RH*#D zirH%ovcvI5A}(l5Ms>p&IGjyyoF)gAVinH74(d#w5^Zw}R?TQj@MlN-BBOj7Z z-pzIxoe5j>r_Z&MGXv;K5lJk1{$3gB=OgV+PmAM z_@Y85=nH*)p1$0@!zor5#{`Jw0c&ZK4n0K{*!J1N_18&G8+Zeaa8tEMjgm8l?(_RE zo}z}IIw{wjSo8s(F@5woOqHNPlJzEi8=BhTgk610^&b5**frlgzv78JTRU?38r{*I zeJ!qU#gctU`-pKoc5P+Y03-YTSGMfEJ|EU|e9kDIj14*9DAQbiFfuvM3->;@Z0q@x z()Wg21Xf|~8uA3JOc`Jj%EuS)Zdc-z?(=o=U8D7dUDY)Z;BH@=%M4IFbTRtLz2vho zvM!eF8ASh!`;=Lg&%cmfpTE!-pd6~SJd!pqF{BW3fZB}LVt*ID>)TLN9**{!9(gE2 zp8Qyx%XE!c_F5}^oyBtY0l%rIiNh25ef6j8y_FT_T69QJGDmq{lNa7IZcwYncClU;41!|L4^7ylZW(%6|Kv_OgvSwVMX`g(vQ_!uq*Wva0w< z3R@Z!W-iWCRr&pL&SVNtU}u+!K9U->Q6D|u(w?gF*--90%fpLm6J^$1FY@`#HrXjB zn;gmScCOK$sKI&pjd~YpE&oFK%%)YFC+QnXy_75D9iHH62ccL zZQ7{P=+aO=IaJ;<-`IN;x206rm<#swphn}Ig?=Ad?^!(n~o&_sqnn5_tkIdb7uqr<*;?4F;h;*ZMfaql8l3ubl?jhrXwua_2VK24oplWHlbTb$As$zb8`@=^ zwZZz$f1T9R(~Gh%UK)xcjN6n8zYPjn_yKF}+S|-b$8LX^mPlRw8~2DbYB|;z3(+;j zxY!*B;Xn23xWdvvIH5|EQhFkjHd!oM+;>@(H}}0gYXfbzS<7lcmh$dXZ1?Ibe)^HF z#7^rc8Tba`N-S-eYkstZI-8gaQ;JCWd8y9D=tFw4Ztv8YN zMR-Dn`WP&z+WPa-=dA>N=Eioz&i4^gPoeMD)!!E?H}>XGS91*Uc_D;=Q21X1dP{5J zNjbxAoO+b!L~gs%oR{_j9ky=ySI@=SlF_Z6W0l?7nKn*D15(SYeRG`WjZ?UN2L(+Z zhDdcD8GqEbcTV@b6*hc!^@0^aM>MEKpzMavvUF)V@#i~Q;e7Jc#^4rzE|#Z3)(01Q zeH`5@bCShMqf3cjXBw7i=# zyz%`lvbB zA}`39GtWO>;wo2ITRm@F!R>(6EBB|fTOV-K@u!w9z)KQs;{)=h&nh-d`)Xj@be~ne zYY<&O>(zCEX$F>Db0YUV<_nG^|Mt_ew8Y z9t&A8j@L8wlkCzOT@8Mgc%W5o%_UjXQ2V^daDiXvxM4hmRMwD=>vvhRr)F#xKbDZ! znDnI}>pRa?ayx#8)bmc|TeKe6OhV<(KFEQj=(*YZ0W1?XcD8b#@()H@b}v4Q4cK42 zLH(KzA2DY1p3UTdZN$Nl(^^EI!|SJn^E&p&t)%Ts!ixqGqvwLWn65RKnY8#gstJwJ z4eH0$!$n_W$ga7O+$-d zhX4A7c*5SMKg*f9=3{N&Rq1=rT@ShU?rCq{Q|a1v+kXaIVHDfhX5uqp*E73Ryn=sN z%jM8;`qtsn$|awD19Szi^4CGM*SXchQF(P;x^f5NMUFnktDpW^>&A(>U(bj4UCsd7nW+CCR!1b)if;kYLW4xWsCiGnt{9?-C|M0UulxKnA&!I^N@mB zqI#T=U}R+$-k`Qwf7T0kDY4foOmSY{Tt$nT(9*M7E8bA+t(!d9IsK$eO*c@a{%X2~yD#>a zb1eL$t2WQxnbODfNJLZCo@gCTHS^m#x7EK3u77BsM>@1+t?p_42FT%iK3s8RIdk-O zZSgK6E;j5hKG*QxeSQmdMT@WJx`z{T>}_n-Yg}--gys{vqe70?;uM5WMjiShKNM}w zZe1L|`GY@w+|;V(EpG0+PD9}AHKMk~Xs(EZW*{zN*!BV&36Qb@$w|$0f3n&nSEPY@ z=04%g(wV!eGa)omf&+sVE{s(tL>8*6m*Q|kk(Lg>RH)4yNFE%mS6t|}Gbo%bJI~gP zTkwyKo^1BkPWF2qbUPV0P24j*yXcyykU-bU#|bnL%ea+%J=t?~E@{&C`Yk<)H14v^ zEX}6X(N-1u9~{X3h7G!#e2r+UnJb~UeByu~(-Z+q7osU7i>kW=E(2hxk|)|cF-izO zG^0+%3mBAb3p_C(b3g%OukvZ9CTmoVkbXm=qB=kj!@QfMk{U1JTQRvl)YAOf+)Tgp zmSPVoWoon!#ZOojqLN&c z`DA&ae6937xdFYaT3#rxd}LmqKaEHZQ!Z193r@9PwP<^0)BM^(-3ZrCD)XahudUM6 z%K@mx3f+lFcE5G&7W*5U%QfY7REUovyuLp^;3j*Dt!QrF-rw0`%#zV<(H1!*iN2aO zqkMJQ&ZaD<55m?QF~6KXK?I1H^?s)8$Hy%svUIN}84|-YM;n?}z;d&INs!$tZXJhi z7t}M^Qq4XJrH*K*?9}oB%teIC^gqxd`K&E0=9o%#TEC)1&yF;`lDPTh36V`pp~(|V zB3MOf$__p!Le&sAQ3H=|No@^NT7IKu2*_SI;r7SPq^XJSLKmBb9*>5|9Kqo^9BzQ< zb0gxa?pFs}Pb{8ZIM-$vrEBcgN^!$4BdBXt)6evdas{{cob3k_rt_KWYx$6kT2=`< zqU$hUg2kZn4LH-B-|mvqhD?`< zQQ32CaLGD3%=`VxxC-LMNrbL0CdlPF^%u01@?xefx5`01xhoLe8FL8(SN`_5nG~pg z05^*38dyX?op>we)jAADB{f`E3=5Up>|~Gp+tjF4--19T?5=v+{6t*)6e?3gVq~1m zW=ET?Quq%iH5^Wmb?kUyT;%$7m~sE9*P02j2|w(=M5wE)tevs9w|9?`_i%7myyt@I z+CW@u_jh8Xf#DZvJ^&UW+m#j>h3@sjK{Y}FGkibOrhj-WM4{HK1k`mnRJ z6YZ6)dPq_~vGlxd#d z_}eeu?`%@6BpkvsTtE$h(YHoe>rT_(TcAeall(msh542NA<8KZ( zoA7amR~Z@Wa@`3$K#00bL06JZ~cS`v^c49pXXxq@qJr<9c&aXKB^wiglTNsH1g!MOyBKS zA>>yv;?=iQ^X)Jg>#m<%kLbv`B!2YbV|z6k$qLgE6}U~dNg%Fdj~sj|Z_|j%*|({w zwoj{uDjGM#D?!pcc7d-ZXZM{KY;bj0AY)1?Cp;#*BNg|n1dB9BUi9&@yU6F_wlDNo z#x?ihn%LM_${+Jv!?!!>?A?j9qinperX%Hfm2r$c-7eEtu;hGzjzN5T*I(%X2lkd>9P7rpm@Qm;Rbw zTX$|j_c0#P&`dTW&fWN!jHW6#5`kaam-JY*i{Q)>|-0i9TWR2%hR4 z4=k+RB7dwq491rw{4)&7|9B^&rwnw~fJuLSF@{K~YHw|oh25nR%+_c#_ucnXh*W5p zWxe$%@Ozl*{YlMlapJ>A;}`bG?U$P$=}MD&U8*2;D6CL!p%X5M4Eo!w;yUl_I?Xbi z;63NqW^Sbs6YZl2m#Ehqbi%$#NqOi*uB?C+ALV@*1+c>ZD2;}AIR@{v%h1@^*x-*1 zFPaa;-LZ(kZSPOjC9XQ0xq5Xgw^Ul1VvZTUeyV>_ZXYk^n+}Yt&=tBW|GDQEnyf}1 zjC8zmB+L`jJPuWI=g0HvyuOHdKc#zc6EocH-W0t6pUs%-d&Jdv;N^A&{yPii_js9O zJG#!g3wVH^;md61$>U>V&gl@xl5nt`t^eGKJnP{{budd}qqSZv!@sW-snie~(Xf!O z<{9;+QV?|pHB|5wjOSdlRCI59{;;qeR(9jgi#y?t|Je}z_pNfpNCgWChmzB*DR%vR z;3CCJnH#daxq8%3@U_^t##f@iDjtW~i1prX(GW55{IGDnD7k zsp##BN@kLu2O!aC;QqpCIKt3IKF{kbL(=62nh_|et#@%5u4(JjtY^ETp5{JPqOM|DFjtBSEiAu$ zTt8c}v`7vkI3PCjR=2LPF)vW7whAjhK9S~TRll9w$h!*&5f`4@@0qZjLwl^^ci`il ztJigz0;{JFRECTH`r2{e>ViLQlNTzoE4h|4u7Ed?x6&PUBY)x(?mD;Q)|Xcy`&*u< za^E`ARu)fKz9lyI?o)OH4eG`AG+0^Jf=8S2`3Zqa7S+!w+z%z&jR;@oUHw)Tr$`Ok z6=yHCu__5?wH^1T$J%B*yu-XvB&TlIyiU}TS3?qBY&^~`Tg`QpXc1GEm^TU3a%hc6 zv5=C>NZ?2d!I04Dxwh0!Rt~sDl6rLMG@t#h7eNZ0mriHR57w$tD{RY)g%DEJx6f^T zSy<#YZmF~blk|hj`)2;bz_Npgb_I&qw4dfd=@2Wfb-cG*_lt;-yv@${01T_*mmkRD z-dg60;mcb3f4ehduwMk3P1MBb^o5XIhCaW$@#!Vc?=d;ppS~Q?x()L_`-(%5-Bl(< z;h0F1A6<^+JvV)d5+koLC+UwVFCPz4e57<*^-L9Co#AEkHhDI3h+%!b_hldwR0M3V zGL@dbzb$Jp(y$T+Jx*g?R5D?3bQ#Dp5^<2}-KL}Q9%HS2t4tVc4JJj_w*i9@dR&=+ zna4@`gHT2)%DX(P4BpPKq<|>w4)5`F@-JINzC>EuMzNC{k z;cgMhMUc4QpPy#W<>uz1{mZG~`ECK?4W=eReB|%ZyOb4fqd)mS4&KU_4Gp=OH*9_I z;qJzK$_}_mlE%(P<;yxcY~;$2U|`tu{`@KW<^Nn|`S-pd$yj!K1|V%9)DW*7@ENFI zYP6X7IVd#_NcH*7bhxz}zjEv=Te^w_u-$0}R*hEm@q3hMq=szF>Y!+VEMmJn*XRr8 zu>KzCnRp4D3*+MjBEiAE>p+;ovw&9k+@%|_ZX5jWvPwO$&+n1)r*-vJIuuW^gY01X zO=hiV6a*$zb*5ipzSv$0dhBH-nV%#e#i7LaiItc(wy|ZK)w3+rHh% zMMD7sn3u$MxMX)GC2$qEVKH}zfFms_I*2+J1B7Zv1McW&Jl5ZvPZA~V3PJfKJgHl> zbe3rMvB2waaF;)Ag8cO`_HoehWQLEtgd* zV78z9KBO>tDTXxnF8e5YcZv=ksf6=0AOil3XoVbhh!Z5a$$y+|l(Zp-O4DT1#b{p+ zgxyp|5$*D^h?lv4!+(ZFyU;)0)Ux;Ul5P42G?nSIzpuD`Q5J1&#vVQE z^AKZ1vWE}6ciuEIdU1)+pj7;R-YU`SP~33@eDt?5`N3undlf2otn%?5HXs`6M1Wo} zsw1y4T0+}O~G_Z_qgh8<&ro;TM`XeQfI6T|CL3V`3`e3 zl4f3=LpSt<(K)K~`V0_I1T4(Nd^ISN>(x5*wHA49J$bU(0cDApFqciEE@kX@`O zfOJUnd&Dh2vk0bLa+raO8V@~JRO|k*kv~2`lG>W8cjfBU3M7`<2h299n!gH>dc+7B z#mS;vaCK7a`q^+TC0V47L?avt!`%c)!Olm{N6C5Mi?uMYPuVpPnqJwqX9ydk$FRRB zQj>=C8vW~CKfZsOu|_xQ{I$F7B;Gz2N`I-#*Iim#TL(=b#HrW{B38g7#VR}w% z9+5C)RJ7O^2VRI{oCM7v6*(wd_QAeG#etm}$AYiI$3{(K!Rx zVw+HkA5=nF@=6J;=C`RCvG=CV0!q^2acw*)24WRx@*)ePa6!`k2;Rm_kQ-54C;VIn z$knO~plntFD9rtbm6y~?09QKI%MRbPY6jhyfB3REx^Fy$BbI(x?hX`y!XaHSSz%~# ztiX~A1xipFN_h@ti0SLmc98WI+%y(1W8AXelBS{Mr zUDEyw#eKr>6hZkFfHW6i4hu?fAq73BSwv5TVu)xT&{O?@j#0XX2L%z6Q`z6Q8>ns4 z9860kgnd2#T=>AH7qGH?CJU|DE1NxM01Y4Ql7e`Hpt(7av0pO+s6Ksy<*1l^a~;;! z%^tV*5b*{D@vztE>olqblFHXsX{6|xqM{Pd8$#KkKSEK>h|3Y> zGlt#|HUI~AS+EggGlHPfRg{bndfAxiRsbco{K7y8UA^ znr%EyULMbEIOSEe@_l>2m!bkHyui|x&2aDC85EJp;dmEKwAf*jM~5hCb>=%_)BPMq zCE@pwPSBch0wJ;zLG;YBDgmU52eSZrR!rYJjBtBS0Fpoz&9a%MY`iqbn&X zNwOGgNojLLb)nl&vw$jn9#VZE9ZC-$WsIbat_l?>u~n22Rk+*I0{ts?>kXKq=gPTZ zAxc2?RSuLuSP68;3p?I4BWJ z%P*TtU##~iW827cTLsmijD&MDT%#XhiVUnPQH`;ob68NSg*Yfdn<7}&RY!`8s4h(Q zyfQ8Kx5#D6AqK9l%rsPif}KG3{lqH$8TmO-4b%-F(dqLJRrE|bF}KUUpX07l6}OTo zfBJUpfwrH4+n-$H{XEV_tA@m$Md3c6<{#z8Ehl!~&g&{#JtPFl7OVBjc0#!JhcDx z$N-n}V0UHnW!4;dm#ked*^;`ZO?_veb0~7oGieSm9TsF0G1rkss3!b(fH)p@^6mzU zF7P@iOS}7R42(SCvs}F6>Qynjj(6FeO%}T_M_g4zBQ8wEH25Ckb`%F7jA}D3M|HL; zx5;kVFfOJf5~`8tABk}Bo0Y&a$Z%C#L`#0A)1)VYV9kv`H{`|)oQt}QNqWib?dImj zUJJg6H4!jU1)2%>J?B&pTiC6#9EY!S29T>w8D8 zM?c#?nx=2mt3B{}n*|2EIIHC_ac>(6@-V%9Rh%s(z}*bTzwsh$-p#*w|RLcZop# zYS>`^sxobVx!=>O;nZ%fYI?vKfuq$v#ha*t`Wa00!r{c=HG1Eg@pT%PZQAAe`_5i( z)6?rj0BP6*!afpBYVHep^Y#&#b0S*nA0^0D1(U}+XZd|1g9)OMt+bcj4SFV2jYl)G zq9182kv=M>Zmn0zpgcc>;#y|e|xzGYo0 z0+gl^nekKYl@81UyBg(PM}7tw$&n0|ZFS`_e54`hr}?unn>~{eMEJ zmwK5dPEfK}3Pc$_v&TZonfC4@3qrrU&J>QbaNBOJ7;gg+z29n z^6022LKX?wZ-Fd(9PJXUtrdOiU+muS|A?i22hCBS?Dj2IO0#!J`7ZuYh~0p3)~!Ed zf5_|Amy-ND*5gd{79A~P%EaLWSM5S5H3qIjeRJULTP{7*I~Tr}vR-)vm`6Cjn>esc zArZ)6i3mdsA8-|ilbBUbwlMhY7Ru({o#7>y&-8Yw;Eu>ADMMko#V!s^IShydAqQnw zmJ?GtX3wM_)%tK-PdM+nt5hyxT=e-UK7J=kLWt9<=|2h^I-`5)M^CXw7!Wjry4x|k zP9z6L1jT}>Znh zGUSmFPFGCeg+yxyaH1i2%I@W$A4wIZYFNvHVAnV_aH7fRs@s_qrH1UJ5gcHp|R{kZ*8raA4xrYxo{|l2edY8LacmKHz*u6-YX^c@3X&^Zcf*6 zbK%RWU^n~>2rr86JC=fn3PXH41CcbH&&P%|d8WSK=eJbGI#(YR^IM)R83zy)D1|dH z?w0PdBQPq#`@HU;7VSc4Bn@!qmQpG>+^#r4E)a{Am(MncU`m>n6ihOO(ib6BZ zTa`W38l*fFH>5~6!xtf1?nDAOpBucIkjJ1Vkp!&oNPDKK!`vkjV9o=op^s6?Sg$Yx zA}kgpUL=_|j@1IsA3G-(#LBI!EB~j;VxYvN==aJM-b3ge{mJQVUyDkiLd(hhsu*rS zz4%z#K~^5rko`#n-*w($e2V>up?&W(RVt&06V@^e#&Wi9Enj;ilxbusLp{bMjK1-T<=~&%<9A-$cY=Fv1Eo z5}4hbjW&wJn3-MmH8By+zXJr9OjeA(65%4vc-tkRb>7^1^=Z$ceyJ_x!(3mYD=ifB zB^KYNlR z2?UF031>5XVeyEWSDtA?)ixu-f)}|JjSI%!NLHP@Id34Zwi6i9o!}qtGaIUnAD};wlpP;aW(FYt(?XI5&YlFi-WvqH`{R%7rwKoU{ImpZG=Ge6}n6P(e0G zgoD$9S0H~Dz4rsGj>?;dyY-C3^1!m}30GA6?SWf17BMp_5#f9(s3JxH&y(lr^If8o zfB|q0R9HZd5xCmtdjokSkW1DKD8{LQa4pM)9^hfjrw6`Tlp=s@-D=)Q1xG`gtq)eo zFV>FQS01ne7|CR8K4|NnXU6OTbu0mk-|ttUBHI{wK^VSXsqmFAPY=XRA(}F2-8yeM zvLZERW&^65T!9~-o(0ch75FB0{F9PUcgqFh8VUCS%4|ZwaD76Y@v*D12lmeSTI=yP zWxVy8S)Y@daV#~&ncwvJKM?g66EzB*e@PgxHc1XN{Y_Cg`(|I*Z`Tj#oW5zFTy79Y zllCeMpt9dE9m*Be_peqw2NMdu@EIqF8C8&mBaOAlmE+^%WMBe9Mg6COpW<_JRJ=df zW)xb6PmC@vPLa7Y@$ZrC>82@PvN#QiO?D75w%E@+7O#H*ZgPt|6QU(QPNC>`IR)TA zsSG$DU)EHno-M9-iEXV+s~@ktuNd2Rt}smW0{36f_~u3Syv6St#DDQl({FCnkEK?$ zeA-sCYUwvwIDBn@+OwZhQ!>B4rUA?shEhrRy@`h&`m9V<5yZO>oO}G`qrKeS)yC7? z&#D(d;EP-=8@2Q0YF47|nFZTQC8~eHrSb5>9C$UWU|X_dWnEQV$4Kc0_9bg8<5O5= z$?|_^{4am{Ls5zI^akIK@brSij(H0c+Ao9n7pm^T>t{8jdn8Q2NV^D}c8sunWSLk8 zjNPl(uMc^H)6;7VA{hcGQy?`+lM*w4xg{9bzjj#MGX4mgcojG=%?=qF4Zhu{n@;)p@wdS%%_LSc_%s^$4SN-dx+Kiu>py z0Kv1zL5=ugX#oDD)~KR%*A1)CAYoGibxNjHkB+t>((a31{gVV!87p1OAi?aNd1pZ* z+}mbgl~4d+BReTkWONviwREw)4K9lk=UC?DuBN?LOBq#_A>*l7mwH*!T%A;x&=#sq z>Dw?3J2;)SE2r2?c zXMy^ducH+ z@sO4J_>VeyU0-N4+0spAfO91VH@N|{F+T>ca9hO`eAO;O5Uw#td}4A3fPi`oZ1V9# zT8TVB%4;~No43F(11)$MG}z*#M>#iGs~;eD3Ji3 z#3|BYIZFW?Xd~iO##u(J;+@X<&ZU}$fLtoRVH*!8XfKY=G_Tf%F1WAjR37TD7 zMi6;t1Z^`;jVCt_>g<4*$vJ7ERon~ku>)wdnG`gDoo@h41DxmOF!gm(YK5Pua9?%j zSb**tA@Ho0A$VpMt|4V`{aFF5tU^XkWf0$}s51vdR*z?pFt1>o^o*R&qCyw$q3e$N zladO4Hy0dD4FOeM6@1FhsSho7H3FC4Q<34Z3hc*O+e@%ujECwIx)^D6fW2P2+nT&+ zJ@h!c)8G#^!(^6GLP-=N*x(7kv>!P9$KW7TVhB5b8h6yAU~yl{a~)E+H}jiH>K6wQ z_cu>PLhhiD_XlS0YnJvmh@=tJ>j8x^EF2w%c_g1-(2sZa>>iNYS3rp~(yfDgshl;i zWTFGl@(AQnKLbgIY=r~54ehgVwf+k@ZN%q*(wVRb3df&>9&h6UyxgJo)YMc!_Iz2O zeO3U?a^;VDowpHwnxG{BqtzHBOklY0-aE_OO)Sc8ke~OxL7V^@(PmNyjb>aECD6Y{ zIA3Tw4|+*tkmv%nuH$dFdMzQJ6fj|HYIwa18j=`mv3zAvbPDK5c}>N+7Uywz3+ow( zJdxvMYh3te0mrf>ODThH)I8W~1WQRjCA{&E~Uwj>RIGF)@; z$A4`Rv^Ck75Ge`J34Bi%2>hnd24SlRpM0+RH=klo(4X8|jw_-+2hPB6|F(N29|GT|L<>I*bduGWoFYzy8b^ol_aog{cx{;L3@Vx{%?JX&i0qSe!GSb z1%u9mOmsG&cf}mK3pnN7?f(2Jim?B7d*Ofcx&G_#Vt7QMDQ@qE?R?$Ysz`Q|Qu}Sp zA9G8KREF|){rOW^6@mYHN92F(k^im7`1jld|87_4|3Ajyzq_jc>(%qmQxW)R>j<=b z8QK+m4{Cz8y#h*h%{3ITFpF0vUVelIslyC&mbm3bqy-^l#|fQSuH$X#ICTV^@?191 zp(+d3kRlyKEG9BJnSXh*&k1r`uB!pusIh3>GV^wYpAm<|ut~pt+Ye-r@i!&dgjo>v zKBHDx;7c^vtGP>n(KV29eB!?fft1iahMxG*rV(fvyn!^+E#J4eKX;>5vLN#Q+f``TQ5ZQ@mNosAgxcZ9eGa(e0$V zGK*I>z8QxA6=uzLI%EBRFBt!Ne8%;^UC1ZtTXJDI+P~w*6jV|cQA4R0@Z53qk=?r19JGmi#~Uu! zP5>>I$JU6y81u~UKo+RgQc$ae@hdCQ3c=9s1f^Of2edXIt{OBzA@>anf11@CEr@4a zP!YD@dwCR37$pC0Q}ZG;m;Y9R<=XwHCPdW2J}r0H333z`<*10m{Ry;8dv|U^uh)Uq zncJ@n{~t%0;RsZ))qi1#wJx_@v&_A$=w)VLUz-v}nL4^< zwt)CqaW*E{Iz_R&qc3SNK3`u5g5Ia-~uU%E4HoiD*S2!4nNJA+H^`4IA zIdDGZowY@K_|aH@;)d38Xu=JFhC2G3UG3ahY9_-r{g?aA?o`1Rk%GO3vFSH-Jz!f~ zhp=!oLc`XS-7onYSlJ42m;Av!hZJcJ&{;GYrGpJInE{LdoEly4mqDM>fKPdJ+TgW} z4XNNwsTF}AUOIZMm2=i2<)5ufKh0{jw6uH%hl3fVBTLxl47x@5|HrPkBx7}sOAdGkl+O{0(-sr_R>jV$GEtsK#BrdTa84 zU<;P@Xe)I$y0^A|Zcuv?_24hv5NB;NLlspP&!%!ZcpOiyR9;n z_IJrfOFZPf4^A1rp-zJ?j|El10&CO!`Yg0%yeje@Ree6*uY}EEi<{9qs~9=f5T`AIMkkxOuX}=5nsg|xOs-FhdJ|ELtW4}Va+cx z|8A3)xr<1(e*&N}$O{CZh9@;G`$Ae)C0ACviBAPU_>P-YU$Ckmhhk5WTQIN#vSq7t znN`RLI-rE~;Y?7#n;>eZC7=QbI7V~P{AcDRP-2{g4&v~d9jZJ{14^MzS;o+c2QFFv zJSW~({Kte{Hd93McA!v@(R719_<7Dj>vH!5YFShf(7W7!Oa( zLpb(?8acZQQF`}g5*2+ak!8elJUzc338>&kt^&fl_haT00ols0AT98r!yCwRvli;i zRRxP6bcWNPwnLVN^9G#2NmbEaHlK^CKU9)V$vQAs$xO*+@IXchLFNHc)QOpCd3cZE z&%=do>T9NDPy=&i=G!i;k)s;Kwp{(?9jY&PJeQ`o`pW!9K|r)C(ENdbq&bxdFbIOLU??}`le^#kd%pn1c9Jxf` z%!XXLH-tL%yBNv7)`-&`1KM;HYG?;%Bf6#oca`jvmqQPdY`G+?8&5gT$XZ@@b+US) z1TFTZd7r10P|*e*T#|5b!g`;g zZ&d{!!1c|I^eIH4oeSaG4TObtNu8L3v33|7-0*!nL$&EiVobdB57~^ZDeXFAF;Y}& z?`Jga!yA+u(!0P&&Vvl@4ZcI-jOvm5So9(f;Yd*dArM?}b}P-mykYO<+=QM{bZCV> z5CC!A()XrZ4L|t}u@1B5OLw2k3UiuHck~lPEwmJluTbW3ACA9$N=lPH6z5p);PKe- zKFo>87F!!T!&uS&NJEE*Ow1cTLsdE9V)UIcom)qPqYh(zPmC`sD3^zKRXS<(;cD~fO4H6OK z;0qFMBP-V~4inE84NlxLm0QRz-OI@tncxfvWi&)zI%Z7dIL=}0dU@VuEMdsQd60it z;#UiQm_x&{heaPYaNi8jTk48EyQA@Ia!ep}!}c7c!kW>CGCPhEGpl+%HiQma)4n^h zND1#G!Aj<9E>G#QShi^1>n9i=$#iSPTg@qK>SMCFtD0PRqD*DF$N5ib1_a;GuJ>Xl zoLF+(;W?uV&nESpP%45-dzKh$Tb4cyQLWMdo!nLyMY2zAf{peyqNNZ~f4 z>ZZ%uqYx@YdVpua!9gs~VVSp{%6x>Atps=5g;9`<>p~}>wZdc>G_WhnwN^?x%O5$) z8asl_jeLQitF;``s`>%iQe6g!UlHh>7m7QUT&+~8R25;nN-FV-6kL;`DdhX34MQj| zCaz^7wVsF$KS11b-ZRa9m)eh;MppW`_nEzjf}g4M!yT#`|}!U z&oAH@uBKGjTRYp=pv_mYn94XB0%0q2h}a<(gGqO{JwoRL#pG_s4DM`Xx?c8k)G0C? zY(ctN-VLffX+F;yl&CD?1woYlIDto}o}#*dGr^LHhISO}SwZB_JQ52=p5v)IFpn;% zYz$mV7O${MaMLY;>vt0zm0%1lmFHnxLwMag_M!L#KIE?b)aF#Z%nmSFyad-%Q-*Qz zky!YTNVE7X=8_1=;6ns(%3JP3+Kv7|nJo0|Ubo4|E6+q>5(G zt}yVzERN}wcivpX4`cIJ!7QN+u`v6dyOS2}NUJoQ;Y86-Hdk63SFhUFnlQ!>eXmGK zCAlQ??PfLgO{T|}#h+hLGc^;M^kc>ZA4p<;?~4uq5xRm7B>}seu&H|8jK=z z>3GbcvOHCQ9)D`;W2g0|rBNl;eG2RQP-}$&qkLwM)S-bSIk|J9*wCR+03~rsVKc{0 z8)ej2E@oL9O_@iBAn?K8q8XjZeh;6PJw9*V6^?9aq!{|H&m01+kwsLzxzjw!$|{o# zuV3$=KJi)wRnnCCKIq2eUOM}JNY(WV2P60TM zgTR#M)CJUeykZ|j(*$FFU`ljnkYU6a8Q(7e`UAxf{Ed2YD?dKPDZm_2be7Ti6ZCjK zJ-}F8KsKh7m6hczkEKS{%a9)vp?NO6XB8CYvK$Cw!m}Mxlvl8cZ`^73|G4?^O=aYl z!hy#p0=&5Cm0f81(uQXJ&qIF&|8vx@Pg!D0GOCBQxj)%yWOS=7d<$XWvK-CGn7$U! z^Llawz>}SGo>ItoB5Jd%f;Kx=qUU6KUgz7aP-Lx_4~{R&mo{QF`RBP-7JL9;QrOYS zXjbom4zLMSVAgomvAUsQRqH~>b%7x7{jf`O!O+Ow=F59@&XxEO9C2XOX1KTRAl94r zB34OJ{#9aqwrD+A>`8~(b@b+f%%9djKt|~C<=(xm%9$iwPqhj7fQMtt>bmrd`bhRTJ(lDd^%t%tHY?K~^Ke<;)Q{b3Xc<&o|H(@< z#nAh0;mNTg<5BH}xm}IiA3eF$%V2j}8TUmUOp5Xm%Ih!-_5>n{4ANdO7+oztZAB-k zNzmq?gBn`L-f`L#<}!2xfSb~!UeGMi*=gZV5{Ekn!xW|w?$IukxNOLh$ok?3xXQj% z%Hf@QW=tXvJ@*(T3V1I}CWKtZ>J=p(IwPq1>9fO3UP9UX^W^5JoQYX)I7lqWX~bqJ zKPYQxv=5zef`s0RP~|m=(X>Uh9;fK6>{4HiJHr5Zo36~~mxrJ>Np6qwWHN>6x=Dp0 z^Lh-+K{wSk_*H_&2C2+lD@by&Gs|+Vl+b8#uj2K%c|&+R5NwFl6G~2m7)*NRh%DXi zhQQCWZ-CgmR=MX3@*RP{V)aR#eqvFbXo|!hUQl)UWkx`UvwI;PFRjNCH~Oqo)9OL<#qTyj)5+h{JYp?Y z5GyJLiyOEj=Xf3Hsza2q=w(c-070{^w+-6TIKVrd~JkJ5G t@h~(%nP4oB1Gn@47QesuTJIXO(f3J7&fWIc;Oj7IXEo1coVxzY{{lH%!`%P? diff --git a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh index 51a1328..9f4d50d 100644 --- a/tests/timeline_retrieval/run_locust_timeline_retrieve.sh +++ b/tests/timeline_retrieval/run_locust_timeline_retrieve.sh @@ -100,7 +100,7 @@ TARGET_USER="max" USERS=1 SPAWN_RATE=1 RUN_TIME="5m" -REPORT_NAME="Report_K_1_User_500_Followings_Hybrid_Mode" +REPORT_NAME="Report_25K_1_User_1600_Followings_Hybrid_Mode" REPORT_HTML="${REPORT_NAME}.html" ALB_URL_RESOLVED=$(get_alb_from_terraform)