Skip to content

Commit d0fc096

Browse files
Add Load Balancing Sample (#6)
1 parent 92c32c2 commit d0fc096

File tree

9 files changed

+741
-6
lines changed

9 files changed

+741
-6
lines changed

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22

33
requests
44
setuptools
5+
pandas
6+
matplotlib
57
pytest
68
pytest-cov
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!--
2+
This policy sample calls an existing Azure Container Apps backend with a prioritized set of backends.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<set-backend-service backend-id="aca-backend-pool-web-api-429-prioritized-and-weighted" />
8+
</inbound>
9+
<backend>
10+
<!--Apply load-balancing and retry mechanisms -->
11+
<!--Set count to one less than the number of backends in the pool to try all backends until the backend pool is temporarily unavailable.-->
12+
<retry count="2" interval="0" first-fast-retry="true" condition="@(context.Response.StatusCode == 429 || (context.Response.StatusCode == 503 && !context.Response.StatusReason.Contains("Backend pool") && !context.Response.StatusReason.Contains("is temporarily unavailable")))">
13+
<forward-request buffer-request-body="true" />
14+
</retry>
15+
</backend>
16+
<outbound>
17+
<base />
18+
</outbound>
19+
<on-error>
20+
<base />
21+
</on-error>
22+
</policies>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!--
2+
This policy sample calls an existing Azure Container Apps backend with a prioritized set of backends.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<set-backend-service backend-id="aca-backend-pool-web-api-429-prioritized" />
8+
</inbound>
9+
<backend>
10+
<!--Apply load-balancing and retry mechanisms -->
11+
<!--Set count to one less than the number of backends in the pool to try all backends until the backend pool is temporarily unavailable.-->
12+
<retry count="1" interval="0" first-fast-retry="true" condition="@(context.Response.StatusCode == 429 || (context.Response.StatusCode == 503 && !context.Response.StatusReason.Contains("Backend pool") && !context.Response.StatusReason.Contains("is temporarily unavailable")))">
13+
<forward-request buffer-request-body="true" />
14+
</retry>
15+
</backend>
16+
<outbound>
17+
<base />
18+
</outbound>
19+
<on-error>
20+
<base />
21+
</on-error>
22+
</policies>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!--
2+
This policy sample calls an existing Azure Container Apps backend with a weighted set of backends (50/50).
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<set-backend-service backend-id="aca-backend-pool-web-api-429-weighted-50-50" />
8+
</inbound>
9+
<backend>
10+
<!--Apply load-balancing and retry mechanisms -->
11+
<!--Set count to one less than the number of backends in the pool to try all backends until the backend pool is temporarily unavailable.-->
12+
<retry count="1" interval="0" first-fast-retry="true" condition="@(context.Response.StatusCode == 429 || (context.Response.StatusCode == 503 && !context.Response.StatusReason.Contains("Backend pool") && !context.Response.StatusReason.Contains("is temporarily unavailable")))">
13+
<forward-request buffer-request-body="true" />
14+
</retry>
15+
</backend>
16+
<outbound>
17+
<base />
18+
</outbound>
19+
<on-error>
20+
<base />
21+
</on-error>
22+
</policies>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!--
2+
This policy sample calls an existing Azure Container Apps backend with a weighted set of backends (50/50).
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<set-backend-service backend-id="aca-backend-pool-web-api-429-weighted-80-20" />
8+
</inbound>
9+
<backend>
10+
<!--Apply load-balancing and retry mechanisms -->
11+
<!--Set count to one less than the number of backends in the pool to try all backends until the backend pool is temporarily unavailable.-->
12+
<retry count="1" interval="0" first-fast-retry="true" condition="@(context.Response.StatusCode == 429 || (context.Response.StatusCode == 503 && !context.Response.StatusReason.Contains("Backend pool") && !context.Response.StatusReason.Contains("is temporarily unavailable")))">
13+
<forward-request buffer-request-body="true" />
14+
</retry>
15+
</backend>
16+
<outbound>
17+
<base />
18+
</outbound>
19+
<on-error>
20+
<base />
21+
</on-error>
22+
</policies>

samples/load-balancing/charts.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
Module providing charting functions.
3+
4+
This module will likely be moved to the /shared/python directory in the future once it's more generic.
5+
"""
6+
7+
import pandas as pd
8+
import matplotlib.pyplot as plt
9+
from matplotlib.patches import Rectangle as pltRectangle
10+
import matplotlib as mpl
11+
import json
12+
13+
14+
# ------------------------------
15+
# CLASSES
16+
# ------------------------------
17+
18+
# TODO: A specialized barchart for multi-request scenarios should be created and use a more generic base class barchart.
19+
# TODO: BarChart should be a base class for other chart types once it's more generic.
20+
class BarChart(object):
21+
"""
22+
Class for creating bar charts with colored bars based on backend indexes.
23+
"""
24+
25+
def __init__(self, title: str, x_label: str, y_label: str, api_results: list[dict], fig_text: str = None):
26+
"""
27+
Initialize the BarChart with API results.
28+
29+
Args:
30+
api_results (list[dict]): List of API result dictionaries.
31+
"""
32+
33+
self.title = title
34+
self.x_label = x_label
35+
self.y_label = y_label
36+
self.api_results = api_results
37+
self.fig_text = fig_text
38+
39+
def plot(self):
40+
"""
41+
Plot the bar chart based on the provided API results.
42+
"""
43+
self._plot_barchart(self.api_results)
44+
45+
def _plot_barchart(self, api_results: list[dict]):
46+
"""
47+
Internal method to plot the bar chart.
48+
49+
Args:
50+
api_results (list[dict]): List of API result dictionaries.
51+
"""
52+
# Parse the data into a DataFrame
53+
rows = []
54+
55+
for entry in api_results:
56+
run = entry['run']
57+
response_time = entry['response_time']
58+
status_code = entry['status_code']
59+
60+
if status_code == 200 and entry['response']:
61+
try:
62+
resp = json.loads(entry['response'])
63+
backend_index = resp.get('index', 99)
64+
except Exception:
65+
backend_index = 99
66+
else:
67+
backend_index = 99
68+
rows.append({
69+
'Run': run,
70+
'Response Time (ms)': response_time * 1000, # Convert to ms
71+
'Backend Index': backend_index,
72+
'Status Code': status_code
73+
})
74+
75+
df = pd.DataFrame(rows)
76+
77+
mpl.rcParams['figure.figsize'] = [15, 7]
78+
79+
# Define a color map for each backend index (200) and errors (non-200 always lightcoral)
80+
backend_indexes_200 = sorted(df[df['Status Code'] == 200]['Backend Index'].unique())
81+
color_palette = ['lightyellow', 'lightblue', 'lightgreen', 'plum', 'orange']
82+
color_map_200 = {idx: color_palette[i % len(color_palette)] for i, idx in enumerate(backend_indexes_200)}
83+
84+
bar_colors = []
85+
for _, row in df.iterrows():
86+
if row['Status Code'] == 200:
87+
bar_colors.append(color_map_200.get(row['Backend Index'], 'gray'))
88+
else:
89+
bar_colors.append('lightcoral')
90+
91+
# Plot the dataframe with colored bars
92+
ax = df.plot(
93+
kind='bar',
94+
x='Run',
95+
y='Response Time (ms)',
96+
color=bar_colors,
97+
legend=False,
98+
edgecolor='black'
99+
)
100+
101+
# Add dynamic legend based on backend indexes present in the data
102+
legend_labels = []
103+
legend_names = []
104+
for idx in backend_indexes_200:
105+
legend_labels.append(pltRectangle((0, 0), 1, 1, color=color_map_200[idx]))
106+
legend_names.append(f'Backend index {idx} (200)')
107+
legend_labels.append(pltRectangle((0, 0), 1, 1, color='lightcoral'))
108+
legend_names.append('Error/Other (non-200)')
109+
ax.legend(legend_labels, legend_names)
110+
111+
plt.title(self.title)
112+
plt.xlabel(self.x_label)
113+
plt.ylabel(self.y_label)
114+
plt.xticks(rotation = 0)
115+
116+
# Exclude high outliers for average calculation
117+
valid_200 = df[(df['Status Code'] == 200)].copy()
118+
if not valid_200.empty:
119+
# Exclude high outliers (e.g., above 95th percentile)
120+
if not valid_200.empty:
121+
upper = valid_200['Response Time (ms)'].quantile(0.95)
122+
filtered = valid_200[valid_200['Response Time (ms)'] <= upper]
123+
if not filtered.empty:
124+
avg = filtered['Response Time (ms)'].mean()
125+
avg_label = f'Mean APIM response time: {avg:.1f} ms'
126+
plt.axhline(y = avg, color = 'b', linestyle = '--')
127+
plt.text(len(df) - 1, avg, avg_label, color = 'b', va = 'bottom', ha = 'right', fontsize = 10)
128+
129+
# Add figtext under the chart
130+
plt.figtext(0.13, -0.1, wrap = True, ha = 'left', fontsize = 11, s = self.fig_text)
131+
132+
plt.show()

0 commit comments

Comments
 (0)