BDE 3.123.0 Release

Schedule

  • The BDE team announces that the BDE 3.123.0 production release was completed on Thursday, August 31, 2023.

BDE 3.123.0 Highlights

bsl Now Provides an Implementation of bsl::stop_token

BDE 3.123.0 adds <bsl_stop_token.h>, which provides an allocator-aware implementation of the C++20 <stop_token> header.

Stop Tokens and Stop Callbacks

The need for cancelling an outstanding operation often comes up in asynchronous code. BDE 3.xxx.x introduces a general-purpose mechanism for using and implementing such cancellation provided in the <bsl_stop_token.h> header. We intend to expand other BDE library APIs (in particular, those in the BHL HTTP/2 communication library) to make use of this component in the near future.

The <bsl_stop_token.h> header, provides functionality to facilitate cooperative cancellation between threads. The bsl::stop_token class can be used to observe whether any thread has made a cancellation request on a corresponding bsl::stop_source object. A thread that is in possession of a bsl::stop_token object may also use the bsl::stop_callback class template to register callbacks that will be invoked when a cancellation request is made. These classes provide the same API as their std counterparts defined in the C++20 <stop_token> header, except that bsl::stop_callback is allocator-aware, and the programmer can optionally supply an allocator when creating a bsl::stop_source object.

Stop Token Usage From The Library Perspective

A library developer might offer the library clients the option of using bsl::stop_token to cancel asynchronous operations. For example, bsl::stop_token and bsl::stop_callback can be used to implement a condition variable wrapper that allows a wait to be interrupted by a cancellation request. (In C++20, such functionality is available as std::condition_variable_any.) The wrapper must hold a bsl::stop_token object that is used to check whether a cancellation has been requested before entering a wait. The wrapper must also ensure that the thread that requests a cancellation wakes up the condition variable if the condition variable is waiting at the time, by registering a callback that calls broadcast. For simplicity, we will show how to implement one possible signature for the wait method. Note below that the implementation of the wait method registers a callback which will signal the condition variable d_condvar upon cancellation.

class InterruptibleConditionVariable {
  private:
    bslmt::Condition d_condvar;

  public:
    void signal() { d_condvar.signal(); }

    void broadcast() { d_condvar.broadcast(); }

    template <class t_PREDICATE>
    int wait(bslmt::Mutex           *mutex,
             t_PREDICATE             pred,
             const bsl::stop_token&  stopToken)
        // Atomically unlock the specified 'mutex' and suspend execution of
        // the current thread until this condition object is signaled and the
        // invocation of the specified 'pred' subsequently returns 'true' or
        // wait is interrupted by a cancellation request observed by the
        // specified 'stopToken'.  Return 0 on success or cancellation via
        // 'stopToken', and a nonzero value if the wait was interrupted for
        // some other reason.
    {
        // Register a callback to be invoked by the thread that makes a
        // cancellation request on the stop state owned by 'stopToken'.  This
        // callback simply invokes our 'broadcast' method, which will wake up
        // any threads that are waiting below after having called 'wait' on
        // this object.
        bsl::stop_callback stopCb(stopToken, [this] { broadcast(); });

        while (!stopToken.stop_requested()) {
            if (pred()) {
                return 0;                                           // RETURN
            }
            const int ret = d_condvar.wait(mutex);
            if (ret != 0) {
                return ret;                                         // RETURN
            }
        }

        return 0;
    }
};
Stop Token Usage From The Client’s Perspective

To illustrate the intended use of the bsl_stop_token component, consider an application built on top of a (future) BHL HTTP/2 API (which will soon be updated to support bsl::stop_token. This application will send the same HTTP/2 requests to multiple hosts, and then cancel all the remaining outstanding requests as soon as the first response is received from one of the remote hosts.

int multiHostRequest(bhlb::HttpResponse<bhlb::StringBody>       *result,
                     bhls::Connector                            *connector,
                     const bsl::span<const bhlb::Endpoint>&      endpoints,
                     const bhlb::HttpRequest<bhlb::StringBody>&  request)
{
    // Create a stop source and get a stop token from it.
    bsl::stop_source stopSource;
    bsl::stop_token  stopToken = stopSource.get_token();

    bool         resultLoaded = false;
    bslmt::Latch doneLatch(bsl::ssize(endpoints));

    bhlc::HttpResponseHandler handleResponse =
            bhlc::HttpResponseHandler().onResponseReceived<bhlb::StringBody>(
                [&](const bhlb::HttpResponse<bhlb::StringBody>& response) {
                    // Request cancellation of all connection attempts and
                    // any in-flight requests.  Only one thread can call
                    // 'request_stop' and have it return 'true', so
                    // additional synchronization for loading the result is
                    // not necessary.
                    if (stopSource.request_stop()) {
                        // Load the result.
                        *result = response;
                        resultLoaded = true;
                    }

                    // Mark completion.
                    doneLatch.arrive();

                    return 0;
                });

    for (int i = 0; i < bsl::ssize(endpoints); ++i) {
        // Connect to the remote endpoint.
        int rc = connector->connect(
            stopToken,  // Pass the stop token to the 'connect' operation.
            endpoints[i],
            bhlb::ConnectOptions(),
            [&](bhls::CompletionStatus::Enum         status,
                const bsl::shared_ptr<bhls::Socket>& socket) {
                if (bhls::CompletionStatus::e_SUCCESS != status ||
                    stopToken.stop_requested()) {
                    // If connection failed or cancellation was requested
                    // already, do nothing other than mark completion.
                    doneLatch.arrive();
                    return; i                                       // RETURN
                }

                // Create a connection and bind it into its close callback to
                // ensure it's not destroyed until it has stopped.
                auto connection =
                    bsl::make_shared<bhlc::ClientConnection>(socket);
                connection->start(bdlf::BindUtil::bind(bdlf::noOp,
                                                       connection));

                // Shut down the connection on stream closure.
                handleResponse.onStreamClosed(bdlf::BindUtil::bind(
                    bhlc::ClientConnection::shutdown,
                    connection.get()));

                // Send the request, passing it the same stop token.
                int rc = bhlc::HttpMessagingUtil::sendRequest(
                                                            stopToken,
                                                            connection.get(),
                                                            request,
                                                            handleResponse);
                if (0 != rc) {
                    // If sending the request failed, shut down the
                    // connection and mark completion.
                    connection->shutdown();
                    doneLatch.arrive();
                }
            });

        if (0 != rc) {
            // If connection attempt could not be started, mark completion.
            doneLatch.arrive();
        }
    }

    // Wait for all connections to complete.
    doneLatch.wait();

    return resultLoaded ? 0 : -1;
}

Without the stop_token mechanism, cancelling multiple connect-sendRequest call chains would require significantly more bookkeeping and thread synchronization, as well as meticulous work to ensure that cancellation requests cannot be missed, to achieve the same result. In the example below, we illustrate the additional work that would be required without stop_token. Specifically, note the need for storing a multitude of AttemptCancellationHandle and bhlc::ClientConnection objects synchronized by a carefully managed bsmlt::Mutex.

int multiHostRequest(bhlb::HttpResponse<bhlb::StringBody>       *result,
                     bhls::Connector                            *connector,
                     const bsl::span<const bhlb::Endpoint>&      endpoints,
                     const bhlb::HttpRequest<bhlb::StringBody>&  request)
{
    bool         resultLoaded = false;
    bslmt::Latch doneLatch(bsl::ssize(endpoints));

    // Bookkeeping and synchronization required for cancellation
    bslmt::Mutex                                         cancellationMutex;
    bsl::vector<bhls::Connector::AttemptCancellationHandle> cancelHandles(
                                                         endpoints.size());
    bsl::vector<bsl::optional<bhlc::ClientConnection> > connections(
                                                         endpoints.size());
    auto cancelAll = [&] {
        // Request cancellation of all connection attempts and any
        // in-flight requests.  The 'cancellationMutex' must be owned by
        // this thread.

        for (int i = 0; i < bsl::ssize(endpoints); ++i) {
            cancelHandles[i].cancel();
            if (connections[i]) {
                connections[i]->terminate();
            }
        }
    };

    bhlc::HttpResponseHandler handleResponse =
          bhlc::HttpResponseHandler().onResponseReceived<bhlb::StringBody>(
              [&](const bhlb::HttpResponse<bhlb::StringBody>& response) {
                  // Load the result.
                  bslmt::LockGuard<bslmt::Mutex> guard(&cancellationMutex);
                  *result = response;

                  cancelAll();

                  return 0;
              });

    // Hold the mutex to avoid the data race on `cancelHandles`.
    bslmt::LockGuard<bslmt::Mutex> guard(&cancellationMutex);

    for (int i = 0; i < bsl::ssize(endpoints); ++i) {
        // Connect to the remote endpoint.
        int rc = connector->connect(
            &cancelHandles[i],  // Record the cancellation handle.
            endpoints[i],
            bhlb::ConnectOptions(),
            [&, i](bhls::CompletionStatus::Enum         status,
                const bsl::shared_ptr<bhls::Socket>& socket) {
                if (bhls::CompletionStatus::e_SUCCESS != status) {
                    // If connection failed, mark completion.
                    doneLatch.arrive();
                    return;                                       // RETURN
                }

                bslmt::LockGuard<bslmt::Mutex> guard(&cancellationMutex);

                if (resultLoaded) {
                    // If 'result' is already loaded, mark completion.
                    doneLatch.arrive();
                    return;                                       // RETURN
                }

                // Record the connection for lifetime management and
                // cancellation.
                connections[i].emplace(socket);

                // Only mark completion when the connection has closed.
                connections[i]->start(bdlf::BindUtil::bind(
                    bslmt::Latch::arrive,
                    &doneLatch));

                // Shut down the connection on stream closure.
                handleResponse.onStreamClosed(bdlf::BindUtil::bind(
                    bhlc::ClientConnection::shutdown,
                    &*connections[i]));

                int rc = bhlc::HttpMessagingUtil::sendRequest(
                                                          &*connections[i],
                                                          request,
                                                          handleResponse);
                if (0 != rc) {
                    // If sending the request failed, shut down the
                    // connection.
                    connections[i]->shutdown();
                }
            });

        if (0 != rc) {
            // If connection attempt could not be started, mark completion.
            doneLatch.arrive();
        }
    }
    guard.release()->unlock();

    // Wait for all connections to complete.
    doneLatch.wait();

    return resultLoaded ? 0 : -1;
}

bdlb::Variant Support for Perfect Forwarding in createInPlace

BDE 3.123.0 adds support for perfect forwarding in the createInPlace method of bdlb::Variant. This allows one to more efficiently construct objects inside of variants. As an illustration, consider how one might represent an abstract syntax tree for a simple grammar:

#include <bdlb_variant.h>
#include <bsl_memory.h>

using namespace BloombergLP;

// FORWARD DECLARATIONS
struct AddExpression;

using Expression = bdlb::Variant<int, AddExpression>;
    // 'Expression' is an alias to a variant of an 'int' or another
    // 'AddExpression' object. The 'int' alternative represents an integer
    // literal expression, and the 'AddExpression' alternative represents an
    // addition of two sub-expressions.

struct AddExpression {
    // CREATORS
    AddExpression(bsl::unique_ptr<Expression> lhs,
                  bsl::unique_ptr<Expression> rhs);
        // Create a new 'AddExpression' syntax tree node representing the
        // addition of the specified 'lhs' and 'rhs' sub-expressions.
};

Since AddExpression must be constructed with unique pointers to its left- and right-hand side sub-expressions, its constructor arguments cannot be passed in a way that creates a copy. Let’s see how the new facilities in bdlb::Variant can come in handy here:

void makeExampleExpression()
{
    bsl::unique_ptr<Expression> lhs = bsl::make_unique<Expression>(2);
    bsl::unique_ptr<Expression> rhs = bsl::make_unique<Expression>(3);

    Expression add;
    add.createInPlace<AddExpression>(bsl::move(lhs), bsl::move(rhs));
}

We see that the moved-from lhs and rhs are forwarded to the constructor of the AddExpression when creating the add object.

Other New Features

  • The bdlb::NullableAllocatedValue class now has additional operations making it more interface-compatible with bdlb::NullableValue and bsl::optional.

Fixed DRQSs:

Summary

Cycle removal: bslmt: Remove bslmt_threadgroup from bslmt_sluice.t.cpp

C++20 Address build failures due to Appendix C [diff.cpp17.over]

C++20: Correction to compiler version documentation

Second batch of changes to NullableAllocatedValue

Fix bsl::stop_token AIX build warning

Fix bsl::stop_token Solaris build error

clean up more include issues in bde

Please fix BSLA_FALLTROUGH macro for clang cpp03

First batch of changes to NullableAllocatedValue

remove unused and invalid for testing only includes in bde

Please add CANONICAL header annotation to bslstl_ostream.h

Update bslstl_syncbuf to more closely follow BDE conventions

[C++20] Associative container tests don’t compile with msvc VS2019 for C++20

Cycle Removal: bslstl: Remove cycle with string-like types

Cycle Removal: bsls: Remove cycle between bsls_fuzztest and timeinterval

bslstl_iterator.h wrong iter_swap pulled in

Cycle removal: bslmt: Remove cycles introduced by readwriterlockassert

Cycle Removal: bslmf: Remove use of IsAccessibleBaseOf from various tests

Cycle Removal: bdlde: Remove bdlde_hexencoder|decoder cycle

balber_berencoder’s documentation looks wrong

correct bdlb_nullableallocatedvalue.h warning from balb_testmessages.cpp and others

Clean up the NullableAllocatedValue test driver

bdlb::Variant::createInPlace perfect forwarding

bdlma_pool.t.cpp: TC 8 fails on clang due to throw from ‘delete’.

Please address unlikely leak potential in SharedPtrUtil::createUninitialized

Implement ‘bsl::stop_token’