diff --git a/docs/research/download_slowdown_detection.ipynb b/docs/research/download_slowdown_detection.ipynb new file mode 100644 index 00000000..95912296 --- /dev/null +++ b/docs/research/download_slowdown_detection.ipynb @@ -0,0 +1,335 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Thresholds For Anomaly Detection in Download Speed\n", + "\n", + "### Background\n", + "Worker Agent uses Job Attachment module to download input files from an S3 bucket. During the download process, Job Attachment provides progress reports periodically using callback, which include information about the current (or average) transfer rate (in bytes/s). We are looking to implement a feature where, If the transfer rate falls below a certain **speed threshold** for a duration bigger than a specific **time threshold**, the download process is halted. This is to identify transfer speeds slow enough to reasonably imply system or network issues.\n", + "\n", + "### Objective\n", + "The key question is how to determine the hard-coded threshold values for transfer rate and time duration. These values should keep a balance between not prematurely terminating downloads and not allowing them to continue for too long with very low transfer speeds.\n", + "\n", + "### Simulation Approach\n", + "Since we don't have real-world data about the transfer rate yet, we decided to simulate the download process based on the following assumptions:\n", + "\n", + "1. Transfer speed\n", + "We assume the transfer speed at any given time (unless it is within the slowdown duration) follows a normal distribution. The mean for this distribution is derived from average internet speeds of North America, which is 12.5 MB/s. The standard deviation is set as 2.5 (without any specific underlying rationale.)\n", + "\n", + "2. Slowdown occurrence\n", + "For each download session in our simulation, we assume that a single time of significant slowdown will occur. The start time, duration, and rate of this slowdown are randomly determined as below, where `expected end time` is (Transfer size) / (Mean speed). \n", + " - start time: a random value from [0, `expected end time`]\n", + " - end time: a random value from [1, 2 * `expected end time` - start time + 1]\n", + " - We assume that problematic slowdowns could last long enough to raise concerns about time and monetary costs, so we consider durations up to twice the expected total time.\n", + " - slowdown rate: a random value from [99.9%, 99.99%]\n", + " - We assume the speed should drop huge enough to feel almost stopped.\n", + "\n", + "For each simulated download session, we test various combinations of speed and time thresholds.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Parameters and Assumptions" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "MEAN_SPEED = 12.5 * 10**6 # 100 Mbps in B/s\n", + "STD_DEV_FOR_SPEED = 2.5 * 10**6 # MEAN_SPEED / 5\n", + "SLOWDOWN_RATE_RANGE = (0.999, 0.9999) # 99.9% to 99.99% slowdown (e.g., 100 Mbps --> 0.1 Mbps)\n", + "TRANSFER_SIZES = [10 * 10**9] # bytes\n", + "SPEED_THRESHOLDS = [\n", + " 1 * 10**3,\n", + " 2 * 10**3,\n", + " 5 * 10**3,\n", + " 10 * 10**3,\n", + " 50 * 10**3,\n", + " 100 * 10**3,\n", + " 200 * 10**3,\n", + " 500 * 10**3,\n", + " 1 * 10**6,\n", + " 2 * 10**6,\n", + " 5 * 10**6,\n", + " 10 * 10**6,\n", + "] # B/s,\n", + "TIME_THRESHOLDS = [1, 2, 3, 4, 5, 10, 15, 20] # minutes\n", + "MEASURE_INTERVAL = 1 * 60 # seconds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Classes / methods for Running Simulation \n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "import numpy as np\n", + "import random\n", + "\n", + "\n", + "@dataclass\n", + "class SimulationEnv:\n", + " \"\"\"\n", + " Transfer speed profile, start time and duration of delay occurrence\n", + " \"\"\"\n", + "\n", + " transfer_size: int\n", + " speed_profile: list[tuple[float, int]]\n", + " slowdown_speed_mean: float\n", + " slowdown_start: int\n", + " slowdown_end: int\n", + " slowdown_duration: int\n", + "\n", + "\n", + "@dataclass\n", + "class SimulationResult:\n", + " \"\"\"\n", + " A combination of speed threshold and time threshold... and a simulation result.\n", + " \"\"\"\n", + "\n", + " speed_threshold: float\n", + " time_threshold: int\n", + " false_positive: bool\n", + " completed_but_cancelled: bool = False\n", + " worker_time: float = 0 # Time taken to complete the download / max possible time\n", + "\n", + " # Make the class iterable\n", + " def __iter__(self):\n", + " yield self\n", + "\n", + " @classmethod\n", + " def get_average_time(cls, results: List[\"SimulationResult\"]) -> float:\n", + " if not results:\n", + " return 0.0\n", + "\n", + " total = sum(result.worker_time for result in results)\n", + " return total / len(results)\n", + "\n", + "\n", + "def create_simulation_env(\n", + " transfer_size: int,\n", + ") -> SimulationEnv:\n", + " \"\"\"\n", + " Create a simulation environment with a randomized speed profile and\n", + " a single occurance of randomized slowdown.\n", + " \"\"\"\n", + " slowdown_start = random.randint(0, transfer_size / MEAN_SPEED) # seconds\n", + " slowdown_duration = random.randint(\n", + " 1, transfer_size * 2 / MEAN_SPEED - slowdown_start + 1\n", + " ) # seconds\n", + " slowdown_end = slowdown_start + slowdown_duration\n", + " slowdown_rate = random.uniform(SLOWDOWN_RATE_RANGE[0], SLOWDOWN_RATE_RANGE[1])\n", + " slowdown_speed_mean = (1 - slowdown_rate) * MEAN_SPEED # in B/s\n", + " slowdown_speed_std_dev = (1 - slowdown_rate) * 0.1\n", + "\n", + " time = 0\n", + " downloaded_so_far = 0 # bytes\n", + " speed_profile: list[tuple[float, int]] = []\n", + "\n", + " while downloaded_so_far < transfer_size:\n", + " if time >= slowdown_start and time < slowdown_end:\n", + " speed = np.random.normal(slowdown_speed_mean, slowdown_speed_std_dev)\n", + " else:\n", + " speed = np.random.normal(MEAN_SPEED, STD_DEV_FOR_SPEED)\n", + " current_speed = speed\n", + " # calculate how much data is downloaded (cumulative)\n", + " downloaded_so_far += current_speed * 1\n", + " time += 1\n", + " speed_profile.append((speed, transfer_size - downloaded_so_far))\n", + "\n", + " return SimulationEnv(\n", + " transfer_size=transfer_size,\n", + " speed_profile=speed_profile,\n", + " slowdown_speed_mean=slowdown_speed_mean,\n", + " slowdown_start=slowdown_start,\n", + " slowdown_end=slowdown_end,\n", + " slowdown_duration=slowdown_duration,\n", + " )\n", + "\n", + "\n", + "# Run a simulation with thresholds combination\n", + "def run_simulation(\n", + " simulation_env: SimulationEnv,\n", + " measure_interval: int,\n", + " speed_threshold: float,\n", + " time_threshold: int,\n", + ") -> SimulationResult:\n", + " speeds_to_avg = [] # in B/s\n", + " time_in_interval = 0 # in seconds\n", + " worker_time = 0 # seconds\n", + " consecutive_slow_speed_count = 0 # count\n", + " false_positive = None\n", + " total_time_expectation = simulation_env.transfer_size / MEAN_SPEED # seconds\n", + " # Total time taken until the download completes without slowdown detection or cancelling\n", + " max_time = len(simulation_env.speed_profile) # seconds\n", + "\n", + " for i, (speed, _) in enumerate(simulation_env.speed_profile):\n", + " speeds_to_avg.append(speed)\n", + " time_in_interval += 1\n", + " worker_time += 1\n", + "\n", + " if time_in_interval >= measure_interval:\n", + " # calculate avg speed over measure_interval\n", + " assert len(speeds_to_avg) > 0\n", + " avg_speed = sum(speeds_to_avg) / len(speeds_to_avg) # bytes/s\n", + " speeds_to_avg = []\n", + "\n", + " # check the measured speed against the threshold\n", + " if avg_speed < speed_threshold:\n", + " consecutive_slow_speed_count += 1\n", + " if consecutive_slow_speed_count >= time_threshold:\n", + " false_positive = (\n", + " i < simulation_env.slowdown_start or i >= simulation_env.slowdown_end\n", + " )\n", + " # After detecting the slowdown and canceling the download, we assume that the user retries\n", + " # from the beginning when the network and speed are back to normal.\n", + " worker_time += total_time_expectation\n", + " return SimulationResult(\n", + " speed_threshold=speed_threshold,\n", + " time_threshold=time_threshold,\n", + " false_positive=false_positive,\n", + " completed_but_cancelled=True,\n", + " worker_time=worker_time / max_time,\n", + " )\n", + " else:\n", + " consecutive_slow_speed_count = 0\n", + " time_in_interval = 0\n", + "\n", + " return SimulationResult(\n", + " speed_threshold=speed_threshold,\n", + " time_threshold=time_threshold,\n", + " false_positive=False,\n", + " completed_but_cancelled=False,\n", + " worker_time=worker_time / max_time,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run Simulation and Visulize Results by Heatmap" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n", + "\n", + "num_simulations = 1000\n", + "results = defaultdict(list)\n", + "for transfer_size in TRANSFER_SIZES:\n", + " for _ in range(num_simulations):\n", + " sim_env = create_simulation_env(transfer_size)\n", + " for speed_threshold in SPEED_THRESHOLDS:\n", + " for time_threshold in TIME_THRESHOLDS:\n", + " key = (speed_threshold, time_threshold)\n", + " # Run a simulation and append the result to dictionary\n", + " results[key].append(\n", + " run_simulation(sim_env, MEASURE_INTERVAL, speed_threshold, time_threshold)\n", + " )\n", + "\n", + " # the list of all speed thresholds used in simulations\n", + " speed_thresholds = [key[0] / 10**3 for key in results.keys()] # KB/s\n", + " # the list of all time thresholds used in simulations\n", + " time_thresholds = [key[1] for key in results.keys()]\n", + " # the list of average times for each combination of speed and time thresholds\n", + " avg_time_list = [\n", + " SimulationResult.get_average_time(results_list) for results_list in list(results.values())\n", + " ]\n", + "\n", + " df = pd.DataFrame(\n", + " {\"Speed thres\": speed_thresholds, \"Time thres\": time_thresholds, \"Avg time\": avg_time_list}\n", + " )\n", + "\n", + " pivot_table = df.pivot_table(values=\"Avg time\", index=\"Time thres\", columns=\"Speed thres\")\n", + "\n", + " # Draw a heatmap\n", + " plt.figure(figsize=(10, 8))\n", + " sns.heatmap(pivot_table, cmap=\"coolwarm\", annot=True, fmt=\".2f\", cbar=True)\n", + " plt.xlabel(\"Speed Threshold (KB/s)\")\n", + " plt.ylabel(\"Time Threshold (Minutes)\")\n", + " plt.title(\"Heatmap of Worker Runtime\")\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sample Results and Analysis" + ] + }, + { + "attachments": { + "image.png": { + "image/png": "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cOFOnTqpQv3t1wDJBRdcoLv07NnTex7ts88+69atW6VKlXSLGjVqqFhqaqrZAGNGcunSpYpddG316tUnT55sns2YUMiiGlSPatOojGbikpKSjGJqmKb24uPjjQZnnKtSN9VH73EppTUhqFv7ZF588cUlSpRQtQrONOpTvnx5xRONGzd+6623zCaZFC+++KLRnnXr1plnjYTa9re//S0uLk4DYMpJS0tTYVWu2lTnv/71r2PHjpmXqDsqrPCoefPmonj11VfNU1kl1FNjsE3zkkYZ5fhMy6paPUrjrDEvrM4OGzasbNmyRYoU6d2796FDh4yzKrl27doffvjBANSjUb7Peinjea1atapt27Z6+jVr1jQWh+mqFi1aqNkKTL/99lujQuP33r17b7/9dvVXj0x9f/PNN73PkkYAgXAWyB/OjaNtCFglcOLEicOHD3vXphkr78Onn3760UcfveGGGwYMGKC/mi+//LLma5YvX27ECh999NGZM2cGDRpUunTpRYsW6eyePXuUqRr0l/7333+fO3euJpS8K1T6vffe0xjMPffco5jpueeeU+Xt27fXH90HH3xwy5YtquT+++83/2Tq73fRokX1x1u/NQen9T0nT57UCnGzTsUTmo9TJTfeeOP06dPVGA0s6a+vWcA7oV4ooLnuuuvuu+++3377bcyYMevXr58xY4bKqJ2vvfaaevH666/rMNOBJUUen376qUIc/V1XFxYvXqzbSUCrrBQOKoZQYxQSDRw4UDUorFTooB7dfffdCvLEoqDk+PHj//73v80mKXrTcqU777xTFSpU1VnzlC5XWKnZQ8UWl156qfJFKo3bbrttyJAh27dvnzBhgh6EIpuYmBjjqo0bNwpBxf75z38qKDGr8pNQSKezJUuW9FPG55QenMqPGjVK1yq2U+8+/PBDlVFap/SYRo4cqUNFPz4XGociUsynoFxR+KRJk5TQ++Hee+8V2k033aQnq6ej8LpYsWIqrwEzjZwJVndR9Pb1118rNtUbQOUzrZxMBBAILwH9l5EXAhEsoL/iWf2T0//7NzquP5b6RJvCKdNh9erVmiQycxRGmKeUUGiiP3s7d+40MjWLpFt4F1AEoBz9UVTQYOSPGDFCORqzUQxn5CgaUDCkCMM49LmFAgWNZ5hnNbyhy1944QWjsKIcff6uXLlyCnSMHO/fK1asUGGF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8gT4Pr7Ie6b0CAEEEEAAAQQQsFGAcSkbcakaAQQQQAABtwmwV6fbnjj9RQABBBBAAAEEciXA2vNc8XExAggggAACCLhcgDk+l78B6D4CCCCAAAKWCrDvuaWcVIYAAggggAACCES4AONSEf6A6R4CCCCAAAJ5KcDa87zU5l4IIIAAAggggICVAj/++GP37t0rVaqkr7KZOXNmVlXPnz+/adOmBQoUqFmz5tSpU81iY8aMufTSS4sVK1auXLlevXpt3LjRPOUnwdpzPzicQgABBBBAAIHsCXg0MGXbT8CmxMfHN27ceOLEiX5Kbt++vVu3bu3atVuxYsW99947YMCAOXPmGOV/+OGHwYMH//rrr3Pnzk1JSenUqZMq9FOVcYo5voBEFEAAAQQQQAABZwh0Offy39bJkydXr179hRdeULF69eotWLBg3LhxnTt31uHs2bPNazVepdGppUuXXnnllWZmpgnGpTJlIRMBBBBAAAEEwk4gKSnppNdLhzlo4sKFCzt27GheqChKOeahmThx4oTSpUqVMnOyShBLZSVDPgIIIIAAAghkXyDaE2Xbj9YzxXm9dJj99kXt37+/fPny5oVKKzxLSEgwc5RIS0vT9N8VV1zRoEED7/xM08zxZcpCJgIIIIAAAgiEncCIESOGDRtmNkuLx820tQmtmlqzZo2m/4KpllgqGCXKIIAAAggggEBQAh479+pU8JT7+KlChQoHDhwwO6N08eLFCxUqZObcfffdX375pT4SeMEFF5iZfhLM8fnB4RQCCCCAAAIIRJpAy5Ytv/vuO7NX+siecozD9PR0BVIzZsz4/vvvtT7dLOM/QSzl34ezCCCAAAIIIOAYgdOnT2unA73UYu19oMSuXbuU1uRgv379jG4MHDhw27Ztw4cP37BhwyuvvDJ9+vShQ4capzS19+67706bNk1bTGlZlV4+66iMYj6/mePzAeEQAQQQQAABBHIuENp9z5csWaKNo4zWGyur+vfvr90N9u3bZwRVOqUBp1mzZil+Gj9+vGbxXn/9dWNDBJ2aNGmSfl911VX6bbymTJly6623/u8o8/+1PZZSQKe9GfSRwvr165tNSExMVBhoRohmvhL6fKP3RxyTk5JibVtZ5n1f0ggggAACCCDgdAGFQZqny9gL783NdVbFli9fnrFYptdmLOaTY+8c36ZNm7QLlja5atiw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veWXKvcfAQEQAREQAREQAREQgeIQSJ1fiock33vvveXLl9etW7d9+/ZVqlQpjp4qKwIiIAIiIAIiIALpSCC5tlSHDh3Gjx+/3377bdq0ifSiRYtq1669cePGrKysefPm1alTJx2RSCcREAEREAEREIFECRT+rfP4EmXnW2/GjBkFBQVcGjRo0JYtW1asWLF+/fr8/Py999578ODBvlUkFAEREAEREAEREIEMIpBcv5QF8e67744YMeKwww5DUr9+/QceeKBXr172qhIiIAIiIAIiIAJlg0A5jD1Pui1VoUIFPhybN29u1KiR/ZQ0btx47dq1NutM4Mcyriwj3FFQULVaNWcBpUVABERABERABEQgfQgkfd/zHj16XHTRRTt37ly1apUd9rp162rUqGGzzsSwYcOIr7Kv//2f0c6rSouACIiACIiACKQzAfxSyftLz4En1y+Vm5trhp2Tk7N9+3aLYNKkSc2bN7dZZ2LgwIH9+vWzkh9Wb7BpJURABERABERABEQg3Qgk15YaO3as74CHDBlSqVIl30vV/nnZS1Wr/W7TSoiACIiACIiACIhAuhFI+hqf74DZIiEvL8/3koQiIAIiIAIiIAKZS4DtJJP3Sk8spWZLPf/88+lJRFqJgAiIgAiIgAiIQHQCyV3je/31131VWblypa9cQhEQAREQAREQgYwmUFhY7vbqTK4t1alTJ/ZEwNfn/ViYvRK8cklEQAREQAREQAREIIMIJHeNj6P3Jk+ejInqei1dujSDGElVERABERABERABEQgikFxbKjs7e8mSJd6+g5xV3pKSiIAIiIAIiIAIZBCB5G0ulbY7qid3ja9///7btm3zfgLY93zOnDleuSQiIAIiIAIiIAIikFkEkmtLtW3b1hcHZxu3a9fO95KEIiACIiACIiACmUuAXc8zV/nENE/uGl9iOqmWCIiACIiACIiACGQKgeT6pTKFgvQUAREQAREQARGIhUDaRjXFMjrfRuSX8sUioQiIgAiIgAiIgAhEIiBbKhImFRIBERABERABERABXwJa4/PFIqEIiIAIiIAIiEAiBLTGlwg11REBERABERABERCBcktAfqlyO/UauAiIgAiIgAjET6BQeyLED1UtioAIiIAIiIAIiEDZJaDY87I7txqZCIiACIiACIhA8glojS/5jNWDCIiACIiACJQbAoo9LzdTrYGKgAiIgAiIgAiIQBwE5JeKg6LaEAEREAEREAER+IfA34U6j08fBREQAREQAREQAREQgcgE5JeKjEoFRUAEREAEREAEiiKgeKmiCOm6CIiACIiACIiACIiAg4D2RHDAUFIEREAEREAEREAEiklAa3zFBKbiIiACIiACIiACwQT+1r7nwXB0RQREQAREQAREQAREwE1Afik3EeVFQAREQAREQAQSJlBY+HfCdTO0ouKlMnTipLYIiIAIiIAIiEBaEJAtlRbTICVEQAREQAREQAQylIDW+DJ04qS2CIiACIiACKQjAe17no6zIp1EQAREQAREQAREIG0JyC+VtlMjxURABERABEQg8who3/PMmzNpLAIiIAIiIAIiIAKlSECx56UIX12LgAiIgAiIgAhkPAGt8WX8FGoAIiACIiACIpA+BLTvefrMhTQRAREQAREQAREQgQwgoDW+DJgkqSgCIiACIiACmUKA2PPk/UWBMHr06IYNG1avXr1FixaLFi3yVtm5c+fdd9/dqFEjyjRr1mzGjBm2DBUr7P7q06ePvRqUkC0VREZyERABERABERCBDCPw8ssv9+vXb8iQIUuXLsVOat++/fr1611jGDRo0JgxY0aNGvX111/37t27c+fOn3zyiSmzePHi//77mjlzJsKuXbu6qnuzFf7+O63PzflmxWqv0ukpqbhHYXoq5tKq4h67XJL0zFbftS09FfNqVbH8HYruhRCjpELm8KxU+FeMA09eU5UKdySv8XhbrlCYITeoLe6f53g5xNXaXm26xNVU9HZO6Tg3euHilpz/RrvwKviiTjrppMcff5xihYWFDRo0uOGGGwYMGOCsdfDBB99xxx3W4dSlS5c999xz3LhxzjKk+/btO23atO+//x5HleuSKyu/lAuIsiIgAiIgAiIgAmlKoKCg4HfHi6xT0R07dixZsuSss84ywooVK5JeuHChswxparG6Z4UYUvPnz7dZk6AprKsrr7yySEOK8rKlXPSUFQEREAEREAERSFMCw4YN28/xIutU9Jdfftm1a9eBBx5ohaTXrVtnsybBwt/DDz+MwwnHFQt5kydPZlnPVea111779ddfe/To4ZL7Z1njK1evP//8k2VU3tN/1JmiaqboyYxniqrSM/avp5DGizRTeOpbH++8p0NrfPZ+c7zIOrVas2YNts6CBQussH///ief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yvM71LlzZwxr26/zIxEyHFs+4UQCDDdu3Ij9xP6iuHl69uzJz7PpHZsbtWnQZENGl4C2IV+ZkI5++OEHgtVYweffAH72cKKYrs2oUdhkg0aUgJ4JzHUqlYxxuoHD/m2nnHIK3zLuD1j8Fhd3M+bLZoMGaAt4E/FON+0zcN97PncD7glWgaAR2QKuRIzTTctJUtKls7IxEqhAW94PqyQiIAIiIAIiIAIiIAJRCJTreKkogFRGBERABERABERABEIIyJYKgaNLIiACIiACIiACIlAEAdlSRQDSZREQAREQAREQAREIISBbKgSOLomACIiACIiACIhAEQRkSxUBSJdFQAREQAREQAREIISAbKkQOLokAiIgAiIgAiIgAkUQkC1VBCBdFgEREAEREAEREIEQArKlQuDokgiIgAiIgAiIgAgUQUC2VBGAdFkE0p8A20lXqFCBA1Kiq8qBORyVE7189JJDhw5lX+no5SnJ8UTo7zzd2VYP1/OZZ57hZBhbOEkJThbi0PFt27YlqX01KwIikOkEZEtl+gxK//QisGHDBk4Q4zgaztPgZOX27dtzgmGpqGgMLGwU7wsDpVRUirfTP//8kwOCOPrDNOuy4d5//32Mxb59+3K0A5csBI6/bNu27dy5c13KcFbjrFmzXEKT5RTkli1bcqyn71UJRUAEREC2lD4DIhAngS5dunCQ9vPPP//dd9+9/vrrp512Gke8xdlB5LYaNGhgD8zmBDqOcbRZzg6L2MyOHTsilkx9sYkTJ3IOYJs2bbxdc2AlVmy/fv1GjhyJFUUBO/yFCxdyDPMFF1zw22+/2Yocfb1582YOl7QSV4LTBp988klO53XJlRUBERABCMiW0sdABGIjwCoV7pAHHnjg9NNP55xUjkkeOHDghRdeaDrgR53fY3PCLmdpYwrYjn/66aeLL74YP0rNmjVzcnJwKdlLTz/99FFHHVW9evUjjzzyiSeesPJFixYdf/zxyE888USsNyu3iUqVKuEYMy+OH65cufK/uYM44tcUe/vtt2mcq+eeey6WlhH26NGjU6dO991338EHH9ykSROEQeqxNscY9957bzTHpsnPz7e9v/DCCw0bNsQJdOmll3LmsZEXFBTceOONBxxwAGpzFO7ixYtteWcCtxmOPXPMc4glOmHChI4dOzormvRLL7100UUXjRgxYvDgwfaqHT5O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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This simulation is based on a lot of assumptions, making it difficult to be a good reflection of real-world situation, and there may be many disagreement in interpreting the results. Nevertheless, some key points that can be derived from these heatmap results are as follows:\n", + "\n", + "- When the speed threshold is set too low (less than 10 KB/s), even if a significant speed degradation occurs from the original speed (99.99% or more), this slowdown itself may not be detected.\n", + "\n", + "- When the time threshold exceeds a certain level (more than 4-5 minutes), it does not seem to have a significant impact on time or cost savings in the context of threshold-based cancellation. (However, as mentioned earlier, caution is required in interpreting these results: we do not assume extremely long durations of slowdown - in this simulation, we limit it to twice the expected download time. Furthermore, we assume that after halting the download, a new session starts to download files from the beginning. Therefore, in some cases, it may appear that it is better to continue downloading until it fully recovers the normal speed, rather than stopping it in the middle.)\n", + "\n", + "- Based on this heatmap, the most suitable range for threshold values appears to be as follows:\n", + " - Speed threshold: 50 - 200 KB/s\n", + " - Time threshold: 1 - 3 minutes\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "deadline-cloud-worker-agent", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/deadline_worker_agent/sessions/session.py b/src/deadline_worker_agent/sessions/session.py index 9a6b7250..e8e20542 100644 --- a/src/deadline_worker_agent/sessions/session.py +++ b/src/deadline_worker_agent/sessions/session.py @@ -765,12 +765,50 @@ def sync_asset_inputs( if self._asset_sync is None: return - def progress_handler(job_upload_status: ProgressReportMetadata) -> bool: + # A transfer rate below 50 Kb/s is considered concerning or potentially stalled. + TRANSFER_RATE_THRESHOLD = 50 * 10**3 # 50 KB/s + # Each progress report callback takes 1 min, so 2 reports amount to 2 mins in total + LOW_TRANSFER_COUNT_THRESHOLD = 2 + low_transfer_count = 0 + + def progress_handler(job_attachment_download_status: ProgressReportMetadata) -> bool: + """ + Callback for Job Attachments' sync_inputs() to track the the file transfer progress. + It performs checks on the tarnsfer rate and decides whether to continue the process. + + Args: + job_attachment_download_status: contains information about the currenet progress. + + Returns: + True if the operation should continue as normal or False to cancel. + """ + # Check the trasfer rate from the progress report. Counts the successive low transfer + # rates, and if the count exceeds the spcified threshold, cancels the download and + # fails the current (sync_input_job_attachments) action. + nonlocal low_transfer_count + transfer_rate = job_attachment_download_status.transferRate + if transfer_rate < TRANSFER_RATE_THRESHOLD: + low_transfer_count += 1 + else: + low_transfer_count = 0 + if low_transfer_count >= LOW_TRANSFER_COUNT_THRESHOLD: + cancel.set() + self.update_action( + action_status=ActionStatus( + state=ActionState.FAILED, + fail_message=( + "Input syncing failed due to successive low transfer rates (< 50 Kb/s). " + "The transfer rate was below the threshold for the last two checks." + ), + ), + ) + return False + self.update_action( action_status=ActionStatus( state=ActionState.RUNNING, - status_message=job_upload_status.progressMessage, - progress=job_upload_status.progress, + status_message=job_attachment_download_status.progressMessage, + progress=job_attachment_download_status.progress, ), ) return not cancel.is_set() diff --git a/test/unit/sessions/test_session.py b/test/unit/sessions/test_session.py index 2992f8b3..ef908681 100644 --- a/test/unit/sessions/test_session.py +++ b/test/unit/sessions/test_session.py @@ -49,6 +49,7 @@ JobAttachmentsFileSystem, PosixFileSystemPermissionSettings, ) +from deadline.job_attachments.progress_tracker import ProgressReportMetadata, ProgressStatus import deadline_worker_agent.sessions.session as session_mod @@ -617,6 +618,52 @@ def test_sync_asset_inputs( else: session.sync_asset_inputs(cancel=cancel, **args) # type: ignore[arg-type] + def test_sync_asset_inputs_cacellation_by_low_transfer_rate( + self, + session: Session, + mock_asset_sync: MagicMock, + ): + """ + Tests that the session is failed if the sync_inputs function reports successive + low transfer rates. + """ + LOW_TRANSFER_COUNT_THRESHOLD = 2 + + # Mock out the AssetSync's sync_inputs function to simulate multiple + # consecutive low transfer rates. + def mock_sync_inputs(on_downloading_files, *args, **kwargs): + low_transfer_rate_report = ProgressReportMetadata( + status=ProgressStatus.DOWNLOAD_IN_PROGRESS, + progress=0.0, + transferRate=10, + progressMessage="", + ) + for _ in range(LOW_TRANSFER_COUNT_THRESHOLD): + on_downloading_files(low_transfer_rate_report) + return ({}, {}) + + mock_asset_sync.sync_inputs = mock_sync_inputs + mock_cancel = MagicMock(spec=Event) + + with patch.object(session, "update_action") as mock_update_action: + session.sync_asset_inputs( + cancel=mock_cancel, + job_attachment_details=JobAttachmentDetails( + manifests=[], + job_attachments_file_system=JobAttachmentsFileSystem.COPIED, + ), + ) + mock_cancel.set.assert_called_once() + mock_update_action.assert_called_with( + action_status=ActionStatus( + state=ActionState.FAILED, + fail_message=( + "Input syncing failed due to successive low transfer rates (< 50 Kb/s). " + "The transfer rate was below the threshold for the last two checks." + ), + ), + ) + class TestSessionInnerRun: """Test cases for Session._run()"""