Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/aiohappyeyeballs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# aiohappyeyeballs Examples

Each sub-directory contains a self-contained example. The order in
which the examples are to appear is specified in `order.json` (an
array of directory names in the expected order).

In each example directory you'll find:

* `config.toml` - must conform to the specification outlined here:
https://docs.pyscript.net/latest/user-guide/configuration/ This is
parsed and ultimately turned into a JSON representation as part of
the package's API object.
* `setup.py` - Python code for contextual and environmental setup,
NOT SEEN BY THE END USER, but is run before the `code.py` code is
evaluated. Allows us to create useful (IPython) shims, avoid
repeating boilerplate and whatnot.
* `code.py` - the actual code added to the editor which forms the
practical example of using the package.
64 changes: 64 additions & 0 deletions examples/aiohappyeyeballs/addr_info_basics/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
A first look at aiohappyeyeballs: shaping addrinfo lists.

The Happy Eyeballs algorithm (RFC 8305) races IPv6 and IPv4 connection
attempts so dual-stack clients fall back gracefully when one family is
slow or unreachable. The aiohappyeyeballs package provides the building
blocks asyncio uses to do this when you already have a list of resolved
addresses (rather than a hostname).

In this first example we'll skip the network entirely and focus on the
addrinfo helpers: the small list-manipulation utilities that let you
prepare and tidy the input to the Happy Eyeballs machinery.
"""
from IPython.core.display import display, HTML
import socket

heading("A handful of resolved addresses for example.org")
note(
"Normally you'd get this list from <code>loop.getaddrinfo()</code> or a "
"DNS cache. We'll build it by hand so we can see exactly what the helpers do."
)

# Each entry is the same 5-tuple shape that socket.getaddrinfo returns:
# (family, type, proto, canonname, sockaddr).
addr_infos = [
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2606:2800:220:1::1", 80, 0, 0)),
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2606:2800:220:1::2", 80, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 80)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.35", 80)),
]
format_addr_infos(addr_infos)

heading("Adding a local bind address with addr_to_addr_infos")
note(
"When you want to bind the outgoing socket to a specific local address, "
"<code>start_connection</code> expects a full addrinfo list, not a tuple. "
"<code>addr_to_addr_infos</code> does that conversion for you."
)
local_addr_infos = addr_to_addr_infos(("127.0.0.1", 0))
format_addr_infos(local_addr_infos)

heading("Interleaving families with pop_addr_infos_interleave")
note(
"Happy Eyeballs prefers to alternate between IPv6 and IPv4. "
"<code>pop_addr_infos_interleave(addr_infos, 1)</code> removes the first "
"address of each family in place — handy after a successful attempt, to "
"skip past addresses you already tried."
)
working = list(addr_infos)
pop_addr_infos_interleave(working, 1)
note("After popping one address per family:")
format_addr_infos(working)

heading("Pruning a known-bad address with remove_addr_infos")
note(
"If you discover an address is unreachable (perhaps from a previous "
"failure), <code>remove_addr_infos</code> strips every entry that matches."
)
working2 = list(addr_infos)
remove_addr_infos(working2, ("93.184.216.34", 80))
note("After removing 93.184.216.34:80")
format_addr_infos(working2)

note(f"aiohappyeyeballs version in use: <code>{aiohappyeyeballs.__version__}</code>")
1 change: 1 addition & 0 deletions examples/aiohappyeyeballs/addr_info_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["aiohappyeyeballs"]
68 changes: 68 additions & 0 deletions examples/aiohappyeyeballs/addr_info_basics/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Shim IPython's display API onto PyScript so example code written in a
Jupyter/IPython idiom runs unmodified in the browser.
"""

import sys
import types
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
"""Wrap pyscript.display so output lands in the example target."""
return _display(
*args, **kwargs, target=__pyscript_display_target__,
)


ipython = types.ModuleType("IPython")
core = types.ModuleType("IPython.core")
core_display = types.ModuleType("IPython.core.display")
core_display.display = display
core_display.HTML = HTML
ipython.core = core
core.display = core_display
ipython.get_ipython = lambda: None
ipython.display = core_display
sys.modules["IPython"] = ipython
sys.modules["IPython.core"] = core
sys.modules["IPython.core.display"] = core_display
sys.modules["IPython.display"] = core_display


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)


import socket
import aiohappyeyeballs
from aiohappyeyeballs import (
addr_to_addr_infos,
pop_addr_infos_interleave,
remove_addr_infos,
)


def format_addr_infos(addr_infos):
"""Render a list of getaddrinfo-style 5-tuples as an HTML table."""
rows = ["<tr><th>family</th><th>type</th><th>proto</th><th>address</th></tr>"]
family_names = {socket.AF_INET: "AF_INET", socket.AF_INET6: "AF_INET6"}
type_names = {socket.SOCK_STREAM: "SOCK_STREAM", socket.SOCK_DGRAM: "SOCK_DGRAM"}
for family, type_, proto, _canon, sockaddr in addr_infos:
rows.append(
"<tr>"
f"<td>{family_names.get(family, family)}</td>"
f"<td>{type_names.get(type_, type_)}</td>"
f"<td>{proto}</td>"
f"<td><code>{sockaddr}</code></td>"
"</tr>"
)
table = "<table border='1' cellpadding='4' cellspacing='0'>" + "".join(rows) + "</table>"
display(HTML(table), append=True)
115 changes: 115 additions & 0 deletions examples/aiohappyeyeballs/happy_eyeballs_simulation/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# ---------------------------------------------------------------------
# Simulating the Happy Eyeballs race
# ---------------------------------------------------------------------
#
# In production you'd hand a list of addrinfos to
# `aiohappyeyeballs.start_connection(...)` along with an event loop, and
# it would race connection attempts across address families, staggering
# each new attempt by `happy_eyeballs_delay` seconds (default 0.25s per
# RFC 8305) until one succeeds.
#
# We can't open real sockets here, but we can simulate the race itself
# to build intuition for what the algorithm does. Each "address" gets a
# pretend connect-time and success flag; we then schedule attempts using
# the same staggered pattern that start_connection uses internally.

heading("A scenario: one slow IPv6 address, one fast IPv4 address")
note(
"Imagine the first IPv6 address is slow to respond (perhaps the route "
"is congested) but the IPv4 fallback is quick. We'll order the addrinfos "
"the way getaddrinfo typically does — IPv6 first — and let Happy Eyeballs "
"race them."
)

# Each entry: (family, type, proto, canonname, sockaddr, connect_seconds, will_succeed)
scenario = [
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::1", 80, 0, 0), 1.20, True),
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::2", 80, 0, 0), 1.30, True),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("203.0.113.10", 80), 0.40, True),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("203.0.113.11", 80), 0.45, True),
]
addr_infos = [row[:5] for row in scenario]
connect_times = {row[4]: row[5] for row in scenario}
will_succeed = {row[4]: row[6] for row in scenario}


async def simulate_happy_eyeballs(addr_infos, delay=0.25):
"""Race fake connect() calls with a staggered start, like RFC 8305."""
loop = asyncio.get_event_loop()
started_at = loop.time()
log = [] # (sockaddr, start, end, outcome)
winner = None

async def fake_connect(sockaddr):
start = loop.time() - started_at
try:
await asyncio.sleep(connect_times[sockaddr])
except asyncio.CancelledError:
log.append((sockaddr, start, loop.time() - started_at, "cancelled"))
raise
end = loop.time() - started_at
if not will_succeed[sockaddr]:
log.append((sockaddr, start, end, "failed"))
raise OSError("simulated failure")
log.append((sockaddr, start, end, "won"))
return sockaddr

tasks = []
for _family, _type, _proto, _canon, sockaddr in addr_infos:
tasks.append(asyncio.create_task(fake_connect(sockaddr)))
try:
# Give this attempt a head start before launching the next.
await asyncio.wait_for(asyncio.shield(tasks[-1]), timeout=delay)
winner = tasks[-1].result()
break
except asyncio.TimeoutError:
continue # stagger: launch the next address
except OSError:
continue # this attempt failed early; move on

if winner is None:
# Nothing won during staggering; wait for the first to finish.
done, pending = await asyncio.wait(
tasks, return_when=asyncio.FIRST_COMPLETED
)
for task in done:
if not task.cancelled() and task.exception() is None:
winner = task.result()
break
for task in pending:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)

return winner, log


winner, log = await simulate_happy_eyeballs(addr_infos, delay=0.25)
note(f"Winning address: <code>{winner}</code>")

# Visualise the race as a Gantt-style chart.
fig, ax = plt.subplots(figsize=(9, 3.5))
colors = {"won": "seagreen", "cancelled": "lightgray", "failed": "indianred"}
for i, (sockaddr, start, end, outcome) in enumerate(log):
ax.barh(i, end - start, left=start, color=colors[outcome],
edgecolor="black", linewidth=0.5)
ax.text(end + 0.02, i, outcome, va="center", fontsize=9)
ax.set_yticks(range(len(log)))
ax.set_yticklabels([str(entry[0]) for entry in log], fontsize=9)
ax.set_xlabel("Time since start (seconds)")
ax.set_title("Happy Eyeballs race: staggered connection attempts")
ax.invert_yaxis()
fig.tight_layout()
display(fig, append=True)

heading("Pruning a flaky address before the next attempt")
note(
"If your application learns that an address is unhealthy, you can clean "
"the addrinfo list before reusing it. Here we drop the slow IPv6 address "
"and reorder so the IPv4 fallback is tried first."
)
pruned = list(addr_infos)
remove_addr_infos(pruned, ("2001:db8::1", 80, 0, 0))
pop_addr_infos_interleave(pruned, 0) # no-op example: keeps ordering intact
note("Cleaned addrinfo list:")
for entry in pruned:
display(HTML(f"<code>{entry}</code>"), append=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["aiohappyeyeballs", "matplotlib"]
28 changes: 28 additions & 0 deletions examples/aiohappyeyeballs/happy_eyeballs_simulation/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Lightweight setup for the second example: same names as cell 1, no shim."""
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
return _display(*args, **kwargs, target=__pyscript_display_target__)


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)


import socket
import asyncio
import matplotlib.pyplot as plt
import aiohappyeyeballs
from aiohappyeyeballs import (
addr_to_addr_infos,
pop_addr_infos_interleave,
remove_addr_infos,
)
4 changes: 4 additions & 0 deletions examples/aiohappyeyeballs/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"addr_info_basics",
"happy_eyeballs_simulation"
]