Skip to content

TCP Socket.cancel doesn't cause scheduled writes to cancel #446

@Tasty213

Description

@Tasty213

Hi,

I'm trying to cancel any pending writes to a socket once realising the socket on the other end has gone away after not responding to messages. I was hoping to use basic_stream_socket::cancel but after calling it i can still poll the io context and get it to trigger writes i had queued up. I've produced the following unit test demonstrating what i mean.


#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MAIN
#define BOOST_TEST_MODULE "C++ Unit Tests for Application"
#include <boost/test/unit_test.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
#define BOOST_TEST_DYN_LINK
#include <boost/test/data/test_case.hpp>
#include <boost/test/framework.hpp>
#include <boost/asio.hpp>

using namespace boost::unit_test::framework;

BOOST_AUTO_TEST_SUITE(BoostSocketTest)

BOOST_AUTO_TEST_CASE(OnClearCallsCancelsIOContext) {
  // Arrange

  // Create a socket and listen on a port
  boost::asio::io_context child_io_context;
  boost::asio::ip::tcp::socket child_socket(child_io_context);
  boost::asio::ip::tcp::endpoint child_endpoint(boost::asio::ip::tcp::v4(), 5001);
  boost::asio::ip::tcp::acceptor child_acceptor(child_io_context);
  child_acceptor.open(child_endpoint.protocol());
  child_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
  child_acceptor.bind(child_endpoint);
  child_acceptor.listen();
  
  bool accepted = false;
  child_acceptor.async_accept(
    child_socket,
    [&accepted](boost::system::error_code ec)
    {
      if (ec) {
        BOOST_FAIL("failed to listen on child socket");
      } else {
        accepted = true;
      }
      return;
    });


  // Create another socket and connect it to the other one
  boost::asio::io_context parent_io_context;
  boost::asio::ip::tcp::socket parent_socket(parent_io_context);
  boost::asio::ip::tcp::resolver parent_resolver(parent_io_context);
  bool connected = false;
  parent_resolver.async_resolve(
    "localhost", "5001",
    [&connected, &parent_socket](const boost::system::error_code ec, boost::asio::ip::tcp::resolver::results_type results)
    {
        BOOST_TEST(ec == boost::system::errc::success, "failed to resolve");
        boost::asio::async_connect(parent_socket, results,
            [&connected](const boost::system::error_code& ec, const boost::asio::ip::tcp::endpoint& )
            {
                BOOST_TEST(ec == boost::system::errc::success, "failed to connect");
                connected = true;
            });
    });
  
  // Wait for the parent socket to connect to the 
  // child socket once completed the IO Contexts 
  // will have no work left to do so will set their
  // state to stopped
  while (!accepted or !connected) {
    child_io_context.poll();
    parent_io_context.poll();
  }

  BOOST_TEST((accepted && connected), "failed to connect or accept as such cannot simulate interrupted write");

  // Queue a message to be sent from the parent socket to the child socket
  std::string test_data = "test";
  bool callback_was_called = false;
  boost::asio::async_write(parent_socket, boost::asio::buffer((uint8_t*)(test_data.data()), test_data.size()),
      [&callback_was_called](boost::system::error_code ec, size_t) {
        callback_was_called = true;
        // Check the reccieved error code is a connection aborted one
        BOOST_TEST(ec == boost::system::errc::connection_aborted, "callback was not called with boost operation aborted");
        return;
      });

  // Restart the IO context as there is more work to do
  // and fail the test case if it fails to restart
  parent_io_context.restart();
  if(parent_io_context.stopped()){
    BOOST_FAIL("failed to restart parent io context");
  }

  // Act - cancel the message that was queued to be sent
  // this should call the callback and mean there is no
  // work to do at the next poll
  parent_socket.cancel();

  // Assert - that if you poll the io context it doesn't 
  // send and that the callback was called, if it was
  // it will already have checked it reccieved a connection
  // aborted error
  auto executed = parent_io_context.poll();
  BOOST_TEST(executed == 0, "Expected the executor to have done no work");
  BOOST_TEST(callback_was_called == true);
}

BOOST_AUTO_TEST_SUITE_END()

Compiled with
g++ -o BoostSocket_test -ansi -W -Wall -Wextra -Wno-non-virtual-dtor -std=c++23 -pthread -DNDEBUG -fPIC -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE=1 -ggdb -DBOOST_BIND_GLOBAL_PLACEHOLDERS -lboost_unit_test_framewor k BoostSocket_test.cpp
Fails the test as such

[root@9f0cfab1ff7b asio]# g++ -o BoostSocket_test -ansi -W -Wall -Wextra -Wno-non-virtual-dtor -std=c++23 -pthread -DNDEBUG -fPIC -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE=1 -ggdb -DBOOST_BIND_GLOBAL_PLACEHOLDERS -lboost_unit_test_framework BoostSocket_test.cpp
[root@9f0cfab1ff7b asio]# chmod +x BoostSocket_test
[root@9f0cfab1ff7b asio]# ./BoostSocket_test 
Running 1 test case...
BoostSocket_test.cpp(83): error: in "BoostSocketTest/OnClearCallsCancelsIOContext": callback was not called with boost operation aborted
BoostSocket_test.cpp(104): error: in "BoostSocketTest/OnClearCallsCancelsIOContext": Expected the executor to have done no work

*** 2 failures are detected in the test module "C++ Unit Tests for Application"

I've probably just misunderstood what kind of operations get cancelled by a cancel function call, any advice or pointers would be appreciated.

Thanks in advance
George

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions