-
Notifications
You must be signed in to change notification settings - Fork 15
socket::shutdown with tests and documentation #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,6 +74,14 @@ namespace corosio { | |
| class BOOST_COROSIO_DECL socket : public io_stream | ||
| { | ||
| public: | ||
| /** Different ways a socket may be shutdown. */ | ||
| enum shutdown_type | ||
| { | ||
| shutdown_receive, | ||
| shutdown_send, | ||
| shutdown_both | ||
| }; | ||
|
|
||
| struct socket_impl : io_stream_impl | ||
| { | ||
| virtual void connect( | ||
|
|
@@ -82,6 +90,8 @@ class BOOST_COROSIO_DECL socket : public io_stream | |
| endpoint, | ||
| std::stop_token, | ||
| system::error_code*) = 0; | ||
|
|
||
| virtual system::error_code shutdown(shutdown_type) noexcept = 0; | ||
| }; | ||
|
|
||
| struct connect_awaitable | ||
|
|
@@ -271,6 +281,47 @@ class BOOST_COROSIO_DECL socket : public io_stream | |
| */ | ||
| void cancel(); | ||
|
|
||
| /** Disable sends or receives on the socket. | ||
|
|
||
| TCP connections are full-duplex: each direction (send and receive) | ||
| operates independently. This function allows you to close one or | ||
| both directions without destroying the socket. | ||
|
|
||
| @li @ref shutdown_send sends a TCP FIN packet to the peer, | ||
| signaling that you have no more data to send. You can still | ||
| receive data until the peer also closes their send direction. | ||
| This is the most common use case, typically called before | ||
| close() to ensure graceful connection termination. | ||
|
|
||
| @li @ref shutdown_receive disables reading on the socket. This | ||
| does NOT send anything to the peer - they are not informed | ||
| and may continue sending data. Subsequent reads will fail | ||
| or return end-of-file. Incoming data may be discarded or | ||
| buffered depending on the operating system. | ||
|
|
||
| @li @ref shutdown_both combines both effects: sends a FIN and | ||
| disables reading. | ||
|
|
||
| When the peer shuts down their send direction (sends a FIN), | ||
| subsequent read operations will complete with `capy::cond::eof`. | ||
| Use the portable condition test rather than comparing error | ||
| codes directly: | ||
|
|
||
| @code | ||
| auto [ec, n] = co_await sock.read_some(buffer); | ||
| if (ec == capy::cond::eof) | ||
| { | ||
| // Peer closed their send direction | ||
| } | ||
| @endcode | ||
|
|
||
| Any error from the underlying system call is silently discarded | ||
| because it is unlikely to be helpful. | ||
|
|
||
| @param what Determines what operations will no longer be allowed. | ||
| */ | ||
| void shutdown(shutdown_type what); | ||
|
Comment on lines
+284
to
+323
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document shutdown() behavior when the socket is closed. ✏️ Proposed doc tweak /** Disable sends or receives on the socket.
TCP connections are full-duplex: each direction (send and receive)
operates independently. This function allows you to close one or
both directions without destroying the socket.
+
+ If the socket is not open, this function has no effect.🤖 Prompt for AI Agents |
||
|
|
||
| private: | ||
| friend class acceptor; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -679,6 +679,109 @@ struct socket_test | |||||||||||||||||
| s2.close(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Shutdown | ||||||||||||||||||
|
|
||||||||||||||||||
| void | ||||||||||||||||||
| testShutdownSend() | ||||||||||||||||||
| { | ||||||||||||||||||
| io_context ioc; | ||||||||||||||||||
| auto [s1, s2] = test::make_socket_pair(ioc); | ||||||||||||||||||
|
|
||||||||||||||||||
| auto task = [](socket& a, socket& b) -> capy::task<> | ||||||||||||||||||
| { | ||||||||||||||||||
| // Write data then shutdown send | ||||||||||||||||||
| co_await a.write_some(capy::const_buffer("hello", 5)); | ||||||||||||||||||
| a.shutdown(socket::shutdown_send); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Read the data | ||||||||||||||||||
| char buf[32] = {}; | ||||||||||||||||||
| auto [ec1, n1] = co_await b.read_some( | ||||||||||||||||||
| capy::mutable_buffer(buf, sizeof(buf))); | ||||||||||||||||||
| BOOST_TEST(!ec1); | ||||||||||||||||||
| BOOST_TEST_EQ(std::string_view(buf, n1), "hello"); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Next read should get EOF | ||||||||||||||||||
| auto [ec2, n2] = co_await b.read_some( | ||||||||||||||||||
| capy::mutable_buffer(buf, sizeof(buf))); | ||||||||||||||||||
| BOOST_TEST(ec2 == capy::cond::eof); | ||||||||||||||||||
|
Comment on lines
+703
to
+706
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Get context around the failing assertion
sed -n '690,720p' test/unit/socket.cppRepository: cppalliance/corosio Length of output: 1007 🏁 Script executed: #!/bin/bash
# Find testReadAfterPeerClose test
rg -n "testReadAfterPeerClose" test/unit/socket.cpp -A 20 -B 2Repository: cppalliance/corosio Length of output: 1518 🏁 Script executed: #!/bin/bash
# Search for EOF/eof handling patterns in read operations
rg -n "cond::eof|error::eof" --type=cpp | head -30Repository: cppalliance/corosio Length of output: 2573 🏁 Script executed: #!/bin/bash
# Check how EOF is handled in different backends
rg -n "ec.*=.*eof|n.*==.*0" --type=cpp src/ | head -20Repository: cppalliance/corosio Length of output: 2196 🏁 Script executed: #!/bin/bash
# Get the assertion in testReadAfterPeerClose
sed -n '420,430p' test/unit/socket.cppRepository: cppalliance/corosio Length of output: 432 🏁 Script executed: #!/bin/bash
# Find definitions of cond::eof and error::eof
rg -n "enum.*eof|error::eof|cond::eof" --type=cpp -B 2 -A 2 src/ include/ | head -40Repository: cppalliance/corosio Length of output: 2677 🏁 Script executed: #!/bin/bash
# Check how cond::eof and error::eof relate to each other
rg -n "cond::eof|error::eof" --type=hpp include/ -B 1 -A 1 | head -50Repository: cppalliance/corosio Length of output: 90 🏁 Script executed: #!/bin/bash
# Find cond::eof and error::eof definitions
rg -n "cond|error" include/boost/corosio/ --type=cpp -B 1 -A 1 | grep -E "eof|namespace|enum|struct|class" | head -60Repository: cppalliance/corosio Length of output: 1212 🏁 Script executed: #!/bin/bash
# Look for condition codes or error codes definitions
fd -e h -e hpp include/ | xargs rg "cond::|error::" -lRepository: cppalliance/corosio Length of output: 1707 🏁 Script executed: #!/bin/bash
# Search for any file mentioning cond::eof or difference between condition and error
rg -n "cond::eof" . --type=cpp -B 3 -A 3Repository: cppalliance/corosio Length of output: 1916 🏁 Script executed: #!/bin/bash
# Check socket.hpp documentation for shutdown behavior
sed -n '300,320p' include/boost/corosio/socket.hppRepository: cppalliance/corosio Length of output: 766 🏁 Script executed: #!/bin/bash
# Find where cond::eof is defined
rg -n "namespace.*cond|class.*eof|cond\s*=" include/ src/ --type=cppRepository: cppalliance/corosio Length of output: 45 🏁 Script executed: #!/bin/bash
# Check error codes and condition codes definitions
rg -n "error_code|condition_code" include/boost/corosio/ -A 3 | head -50Repository: cppalliance/corosio Length of output: 3247 🏁 Script executed: #!/bin/bash
# Look for the definitions of cond and error namespaces/categories
find include/boost/corosio -name "*.hpp" -o -name "*.h" | xargs grep -l "cond::" | head -5Repository: cppalliance/corosio Length of output: 233 🏁 Script executed: #!/bin/bash
# Get the full context of io_stream.hpp to understand cond::eof better
cat include/boost/corosio/io_stream.hpp | head -100Repository: cppalliance/corosio Length of output: 3730 🏁 Script executed: #!/bin/bash
# Search for cond namespace and how it relates to error codes
find include/boost/corosio -type f | xargs rg "cond" | head -30Repository: cppalliance/corosio Length of output: 2570 🏁 Script executed: #!/bin/bash
# Look for any condition code category or definition
rg -n "condition|cond" include/boost/capy --type=cpp | head -50Repository: cppalliance/corosio Length of output: 121 🏁 Script executed: #!/bin/bash
# Check if there's any sys condition or condition code category defined
rg -n "struct.*cond|enum.*cond|make_error_code.*cond" . --type=cpp -B 2 -A 2 | head -50Repository: cppalliance/corosio Length of output: 521 🏁 Script executed: #!/bin/bash
# Look for how cond::eof is being used/compared in the codebase
rg "cond::eof" . --type=cpp -B 5 -A 5Repository: cppalliance/corosio Length of output: 2521 🏁 Script executed: #!/bin/bash
# Look for all EOF assertions in tests to see the pattern
rg "cond::eof|error::eof|n.*==.*0" test/unit/socket.cpp -B 2 -A 1Repository: cppalliance/corosio Length of output: 756 🏁 Script executed: #!/bin/bash
# Check if capy::cond is defined in this repo or external
find . -name "*.hpp" -o -name "*.h" | xargs grep -l "namespace capy" | head -10Repository: cppalliance/corosio Length of output: 102 🏁 Script executed: #!/bin/bash
# Look for how shutdown_send and close differ
rg -n "shutdown_send|shutdown_receive" include/boost/corosio/socket.hpp -B 5 -A 5Repository: cppalliance/corosio Length of output: 1275 🏁 Script executed: #!/bin/bash
# Check if there's any code path that returns {ec={}, n=0} vs cond::eof
rg -n "ec_out.*=.*NULL|bytes_transferred.*==.*0" src/corosio/src/detail/ --type=cpp -B 3 -A 3 | head -50Repository: cppalliance/corosio Length of output: 1275 🏁 Script executed: #!/bin/bash
# Check if there are any tests that explicitly verify both error::eof and cond::eof equivalence
rg "error::eof|cond::eof" test/unit/ --type=cpp -B 3 -A 3 | grep -A 3 -B 3 "BOOST_TEST"Repository: cppalliance/corosio Length of output: 1364 🏁 Script executed: #!/bin/bash
# Look at the complete testShutdownSend function to understand if there's any special handling
sed -n '688,715p' test/unit/socket.cppRepository: cppalliance/corosio Length of output: 951 🏁 Script executed: #!/bin/bash
# Verify if other shutdown tests use the same assertion pattern
rg -n "testShutdown" test/unit/socket.cpp -A 30 | grep -E "testShutdown|BOOST_TEST.*eof|BOOST_TEST.*n.*=="Repository: cppalliance/corosio Length of output: 317 🏁 Script executed: #!/bin/bash
# Search for where capy::cond and capy::error are defined or aliased
find . -type f \( -name "*.hpp" -o -name "*.h" -o -name "*.cpp" \) -exec grep -l "cond\|error" {} \; | grep -E "capy|corosio" | head -20Repository: cppalliance/corosio Length of output: 839 🏁 Script executed: #!/bin/bash
# Look for any mapping or equivalence between cond::eof and error::eof
rg "cond|error" . --type=cpp -B 2 -A 2 | grep -E "using|typedef|==" | head -30Repository: cppalliance/corosio Length of output: 2294 🏁 Script executed: #!/bin/bash
# Check testShutdownReceive and testShutdownOnClosedSocket for their EOF assertions
sed -n '716,770p' test/unit/socket.cpp | grep -E "BOOST_TEST|eof|ec"Repository: cppalliance/corosio Length of output: 595 🏁 Script executed: #!/bin/bash
# Look for the actual implementation of read_some to see what it returns
rg -n "read_some.*{|co_return.*ec" src/corosio/src/detail/ --type=cpp -A 3 | head -50Repository: cppalliance/corosio Length of output: 183 🏁 Script executed: #!/bin/bash
# Look for socket implementation details about read_some
rg -n "read_some" src/corosio/src/detail/ --type=cpp -A 5 | head -60Repository: cppalliance/corosio Length of output: 2656 🏁 Script executed: #!/bin/bash
# Check what capy is - is it an external library?
find . -name "*.hpp" -o -name "*.h" | xargs grep "namespace capy" | head -5Repository: cppalliance/corosio Length of output: 119 🏁 Script executed: #!/bin/bash
# Get full testShutdownReceive to see EOF assertions
sed -n '716,760p' test/unit/socket.cppRepository: cppalliance/corosio Length of output: 1238 🏁 Script executed: #!/bin/bash
# Look for any documentation about cond vs error in capy library
rg -n "portable|condition.*test|cond::" include/boost/corosio/ --type=cpp -B 2 -A 2 | head -80Repository: cppalliance/corosio Length of output: 5617 🏁 Script executed: #!/bin/bash
# Search for capy library - is it external or in this repo?
find . -name "*.hpp" -o -name "*.h" | xargs grep -l "namespace capy" | xargs head -20Repository: cppalliance/corosio Length of output: 636 🏁 Script executed: #!/bin/bash
# Look for error code category definitions to understand cond vs error equivalence
rg -n "error_category|condition_check|operator==" include/boost/corosio/ --type=cpp -B 2 -A 2 | head -60Repository: cppalliance/corosio Length of output: 1507 🏁 Script executed: #!/bin/bash
# Check if there's any special handling that converts error::eof to cond::eof
rg -n "error::eof.*cond|cond::eof.*error" . --type=cpp -B 2 -A 2Repository: cppalliance/corosio Length of output: 45 🏁 Script executed: #!/bin/bash
# Look for any test utilities that might explain the difference
cat test/unit/socket.cpp | head -100Repository: cppalliance/corosio Length of output: 2397 🏁 Script executed: #!/bin/bash
# Get the first 50 lines of test/unit/socket.cpp to see all imports
head -50 test/unit/socket.cppRepository: cppalliance/corosio Length of output: 1340 🏁 Script executed: #!/bin/bash
# Now understand the relationship - search for how capy::cond::eof and capy::error::eof relate
# Check if cond provides portable comparison operators
rg -n "cond::eof|error::eof" . --type=cpp | wc -lRepository: cppalliance/corosio Length of output: 65 🏁 Script executed: #!/bin/bash
# Let me compare all three EOF test cases side by side
echo "=== testReadAfterPeerClose ===" && sed -n '403,430p' test/unit/socket.cpp
echo -e "\n=== testReadPartialEOF ===" && rg -A 20 "testReadPartialEOF" test/unit/socket.cpp | head -30
echo -e "\n=== testShutdownSend ===" && sed -n '685,710p' test/unit/socket.cppRepository: cppalliance/corosio Length of output: 2853 🏁 Script executed: #!/bin/bash
# Count how EOF is checked across all shutdown/close tests
echo "=== EOF handling patterns ===" && \
rg "testRead.*Close|testShutdown" test/unit/socket.cpp -A 25 | grep -E "testRead|testShutdown|BOOST_TEST.*ec.*eof|BOOST_TEST.*n.*==" | head -20Repository: cppalliance/corosio Length of output: 502 🏁 Script executed: #!/bin/bash
# Verify what the composed read() operation does with EOF
sed -n '630,680p' test/unit/socket.cpp | grep -E "testRead|BOOST_TEST.*eof"Repository: cppalliance/corosio Length of output: 183 EOF assertion should accept zero bytes for robustness. - BOOST_TEST(ec2 == capy::cond::eof);
+ BOOST_TEST(ec2 == capy::cond::eof || n2 == 0);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it should not accept zero bytes, that would be a postcondition violation. The contract says eof. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The difference between ✏️ Learnings added
|
||||||||||||||||||
| }; | ||||||||||||||||||
| capy::run_async(ioc.get_executor())(task(s1, s2)); | ||||||||||||||||||
|
|
||||||||||||||||||
| ioc.run(); | ||||||||||||||||||
| s1.close(); | ||||||||||||||||||
| s2.close(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| void | ||||||||||||||||||
| testShutdownReceive() | ||||||||||||||||||
| { | ||||||||||||||||||
| io_context ioc; | ||||||||||||||||||
| auto [s1, s2] = test::make_socket_pair(ioc); | ||||||||||||||||||
|
|
||||||||||||||||||
| auto task = [](socket& a, socket& b) -> capy::task<> | ||||||||||||||||||
| { | ||||||||||||||||||
| // Shutdown receive on b | ||||||||||||||||||
| b.shutdown(socket::shutdown_receive); | ||||||||||||||||||
|
|
||||||||||||||||||
| // b can still send | ||||||||||||||||||
| co_await b.write_some(capy::const_buffer("from_b", 6)); | ||||||||||||||||||
|
|
||||||||||||||||||
| char buf[32] = {}; | ||||||||||||||||||
| auto [ec, n] = co_await a.read_some( | ||||||||||||||||||
| capy::mutable_buffer(buf, sizeof(buf))); | ||||||||||||||||||
| BOOST_TEST(!ec); | ||||||||||||||||||
| BOOST_TEST_EQ(std::string_view(buf, n), "from_b"); | ||||||||||||||||||
| }; | ||||||||||||||||||
| capy::run_async(ioc.get_executor())(task(s1, s2)); | ||||||||||||||||||
|
|
||||||||||||||||||
| ioc.run(); | ||||||||||||||||||
| s1.close(); | ||||||||||||||||||
| s2.close(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| void | ||||||||||||||||||
| testShutdownOnClosedSocket() | ||||||||||||||||||
| { | ||||||||||||||||||
| io_context ioc; | ||||||||||||||||||
| socket sock(ioc); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Shutdown on closed socket should not crash | ||||||||||||||||||
| sock.shutdown(socket::shutdown_send); | ||||||||||||||||||
| sock.shutdown(socket::shutdown_receive); | ||||||||||||||||||
| sock.shutdown(socket::shutdown_both); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| void | ||||||||||||||||||
| testShutdownBothSendDirection() | ||||||||||||||||||
| { | ||||||||||||||||||
| io_context ioc; | ||||||||||||||||||
| auto [s1, s2] = test::make_socket_pair(ioc); | ||||||||||||||||||
|
|
||||||||||||||||||
| auto task = [](socket& a, socket& b) -> capy::task<> | ||||||||||||||||||
| { | ||||||||||||||||||
| // Write data then shutdown both | ||||||||||||||||||
| co_await a.write_some(capy::const_buffer("goodbye", 7)); | ||||||||||||||||||
| a.shutdown(socket::shutdown_both); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Peer should receive the data | ||||||||||||||||||
| char buf[32] = {}; | ||||||||||||||||||
| auto [ec1, n1] = co_await b.read_some( | ||||||||||||||||||
| capy::mutable_buffer(buf, sizeof(buf))); | ||||||||||||||||||
| BOOST_TEST(!ec1); | ||||||||||||||||||
| BOOST_TEST_EQ(std::string_view(buf, n1), "goodbye"); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Next read should get EOF | ||||||||||||||||||
| auto [ec2, n2] = co_await b.read_some( | ||||||||||||||||||
| capy::mutable_buffer(buf, sizeof(buf))); | ||||||||||||||||||
| BOOST_TEST(ec2 == capy::cond::eof); | ||||||||||||||||||
| }; | ||||||||||||||||||
| capy::run_async(ioc.get_executor())(task(s1, s2)); | ||||||||||||||||||
|
|
||||||||||||||||||
| ioc.run(); | ||||||||||||||||||
| s1.close(); | ||||||||||||||||||
| s2.close(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Data Integrity | ||||||||||||||||||
|
|
||||||||||||||||||
| void | ||||||||||||||||||
|
|
@@ -770,6 +873,12 @@ struct socket_test | |||||||||||||||||
| testReadAfterPeerClose(); | ||||||||||||||||||
| testWriteAfterPeerClose(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Shutdown | ||||||||||||||||||
| testShutdownSend(); | ||||||||||||||||||
| testShutdownReceive(); | ||||||||||||||||||
| testShutdownOnClosedSocket(); | ||||||||||||||||||
| testShutdownBothSendDirection(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Cancellation | ||||||||||||||||||
| testCancelRead(); | ||||||||||||||||||
| testCloseWhileReading(); | ||||||||||||||||||
|
|
||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.