Repository: python-trio/trio

Issue:
Update happy eyeballs for RFC 8305
A new happy eyeballs RFC came out a few months ago: https://tools.ietf.org/html/rfc8305

Golang tracking issue: https://github.com/golang/go/issues/23841

It's interesting. The old one (RFC 6555) was incredibly vague; lots of "well maybe you could do this, or maybe that, there are lots of options ok", then with a paragraph stuck in at the end claiming (incorrectly) to document what browsers actually do. This one has actual concrete recommendations, and apparently corresponds to what MacOS/iOS do. So... anything we should take from it?

They recommend a 250 ms default timeout. Okay, sure, cool. There's some language about adjusting this based on history, within the bounds of 100 ms and 2 s, but I don't think we want to bother tracking history. And, we're not in a great place to do it either – if we were in the kernel or glibc then it would make more sense.

They recommend doing some cleverness with AAAA vs A records during DNS lookup, where if you get a DNS response for your preferred address family first, you should go ahead and start making connections while waiting for the other DNS response. It turns out that you actually can use `getaddrinfo` to do separate AAAA and A lookups (just specify `family=AF_INET6` vs `family=AF_INET`; in glibc at least this does correctly trigger the appropriate DNS queries, confirmed with wireshark). So in principle we could implement this pretty easily... IF we knew the local host's policy for preferring IPv4 vs IPv6. Which we don't, since it's some operating system level tuning parameter and there's no standard API for accessing it. Sigh. Maybe the new RFC will encourage OSes to implement better APIs here? There's some temptation to hard-code a preference for AAAA, because c'mon it's the future, and that might work fine on consumer devices, but I think this might annoy all the folks still using IPv4-only networks in the datacenter, and that's kind of an important target userbase for trio.

In fact a lot of the fancier recommendations really only make sense when the happy eyeballs algorithm is integrated into the OS. Does anyone know how Apple actually exposes their implementation? Is there some API we should be calling on macOS instead of using our implementation? Or do you have to switch over to some totally different networking API instead of BSD sockets?

It looks like in the short term the only action item here might be to switch our default delay to 250 ms. (I'm sort of tempted to refactor our happy eyeballs implementation a bit while I'm in there, to more closely match the later version I use in the talk, and to track unclosed sockets in a more automated way.)


Discussion:
On my system the default is to return v6 addresses first, then v4. This can be changed in `/etc/gai.conf`.
When I remove my v6 default route, `getaddrinfo` will return the v4 address first.

I think you should let `getaddrinfo` just do what it does.
@joernheissler Yeah, that's what I mean when I complain that we can't do anything clever with DNS lookups because we have no reliable way to figure out whether the OS is configured to prefer IPv4 or IPv6.
The "good first issue" part is: Changing the default timeout in `trio.open_tcp_stream` to be 0.250 seconds instead of 0.300 seconds

Resolving patch:
diff --git a/trio/_highlevel_open_tcp_stream.py b/trio/_highlevel_open_tcp_stream.py
--- a/trio/_highlevel_open_tcp_stream.py
+++ b/trio/_highlevel_open_tcp_stream.py
@@ -62,7 +62,7 @@
 #   https://www.researchgate.net/profile/Vaibhav_Bajpai3/publication/304568993_Measuring_the_Effects_of_Happy_Eyeballs/links/5773848e08ae6f328f6c284c/Measuring-the-Effects-of-Happy-Eyeballs.pdf
 # claims that Firefox actually uses 0 ms, unless an about:config option is
 # toggled and then it uses 250 ms.
-DEFAULT_DELAY = 0.300
+DEFAULT_DELAY = 0.250
 
 # How should we call getaddrinfo? In particular, should we use AI_ADDRCONFIG?
 #


Test patch:
diff --git a/trio/tests/test_highlevel_open_tcp_stream.py b/trio/tests/test_highlevel_open_tcp_stream.py
--- a/trio/tests/test_highlevel_open_tcp_stream.py
+++ b/trio/tests/test_highlevel_open_tcp_stream.py
@@ -284,7 +284,7 @@ async def test_one_host_failed_after_connect(autojump_clock):
     assert isinstance(exc, KeyboardInterrupt)
 
 
-# With the default 0.300 second delay, the third attempt will win
+# With the default 0.250 second delay, the third attempt will win
 async def test_basic_fallthrough(autojump_clock):
     sock, scenario = await run_scenario(
         80,
@@ -295,11 +295,12 @@ async def test_basic_fallthrough(autojump_clock):
         ],
     )
     assert sock.ip == "3.3.3.3"
-    assert trio.current_time() == (0.300 + 0.300 + 0.2)
+    # current time is default time + default time + connection time
+    assert trio.current_time() == (0.250 + 0.250 + 0.2)
     assert scenario.connect_times == {
         "1.1.1.1": 0,
-        "2.2.2.2": 0.300,
-        "3.3.3.3": 0.600,
+        "2.2.2.2": 0.250,
+        "3.3.3.3": 0.500,
     }
 
 
@@ -313,10 +314,10 @@ async def test_early_success(autojump_clock):
         ],
     )
     assert sock.ip == "2.2.2.2"
-    assert trio.current_time() == (0.300 + 0.1)
+    assert trio.current_time() == (0.250 + 0.1)
     assert scenario.connect_times == {
         "1.1.1.1": 0,
-        "2.2.2.2": 0.300,
+        "2.2.2.2": 0.250,
         # 3.3.3.3 was never even started
     }
 
@@ -348,16 +349,17 @@ async def test_custom_errors_expedite(autojump_clock):
             ("1.1.1.1", 0.1, "error"),
             ("2.2.2.2", 0.2, "error"),
             ("3.3.3.3", 10, "success"),
-            ("4.4.4.4", 0.3, "success"),
+            # .25 is the default timeout
+            ("4.4.4.4", 0.25, "success"),
         ],
     )
     assert sock.ip == "4.4.4.4"
-    assert trio.current_time() == (0.1 + 0.2 + 0.3 + 0.3)
+    assert trio.current_time() == (0.1 + 0.2 + 0.25 + 0.25)
     assert scenario.connect_times == {
         "1.1.1.1": 0,
         "2.2.2.2": 0.1,
         "3.3.3.3": 0.1 + 0.2,
-        "4.4.4.4": 0.1 + 0.2 + 0.3,
+        "4.4.4.4": 0.1 + 0.2 + 0.25,
     }
 
 
@@ -368,7 +370,7 @@ async def test_all_fail(autojump_clock):
             ("1.1.1.1", 0.1, "error"),
             ("2.2.2.2", 0.2, "error"),
             ("3.3.3.3", 10, "error"),
-            ("4.4.4.4", 0.3, "error"),
+            ("4.4.4.4", 0.250, "error"),
         ],
         expect_error=OSError,
     )
@@ -380,7 +382,7 @@ async def test_all_fail(autojump_clock):
         "1.1.1.1": 0,
         "2.2.2.2": 0.1,
         "3.3.3.3": 0.1 + 0.2,
-        "4.4.4.4": 0.1 + 0.2 + 0.3,
+        "4.4.4.4": 0.1 + 0.2 + 0.25,
     }