Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

(Experimental) Connection pools

This example demonstrates how to use connection_pool. It implements an HTTP REST API server for a text notes application. The API provides CRUD methods on note objects. Instead of opening a new MySQL connection per HTTP request, the server uses a connection pool to reuse connections.

The example employs async functions with stackful coroutines.

This example contains multiple files, and requires linking to Boost.Context, Boost.Json and and Boost.Url. This example assumes you have gone through the setup.

//
// File: main.cpp
//

#include <boost/mysql/any_address.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/pool_params.hpp>

#include <boost/asio/detached.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/system/error_code.hpp>

#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>

#include "server.hpp"

// This example demonstrates how to use a connection_pool.
// It implements a minimal REST API to manage notes.
// A note is a simple object containing a user-defined title and content.
// The REST API offers CRUD operations on such objects:
//    POST   /notes        Creates a new note.
//    GET    /notes        Retrieves all notes.
//    GET    /notes/<id>   Retrieves a single note.
//    PUT    /notes/<id>   Replaces a note, changing its title and content.
//    DELETE /notes/<id>   Deletes a note.
//
// Notes are stored in MySQL. The note_repository class encapsulates
//   access to MySQL, offering friendly functions to manipulate notes.
// server.cpp encapsulates all the boilerplate to launch an HTTP server,
//   match URLs to API endpoints, and invoke the relevant note_repository functions.
// All communication happens asynchronously. We use stackful coroutines to simplify
//   development, using boost::asio::spawn and boost::asio::yield_context.
//
// Note: connection_pool is an experimental feature.

using namespace notes;

// The number of threads to use
static constexpr std::size_t num_threads = 5;

int main(int argc, char* argv[])
{
    // Check command line arguments.
    if (argc != 5)
    {
        std::cerr << "Usage: " << argv[0] << " <username> <password> <mysql-hostname> <port>\n";
        return EXIT_FAILURE;
    }

    // Application config
    const char* mysql_username = argv[1];
    const char* mysql_password = argv[2];
    const char* mysql_hostname = argv[3];
    auto port = static_cast<unsigned short>(std::stoi(argv[4]));

    // An event loop, where the application will run.
    // We will use the main thread to run the pool, too, so we use
    // one thread less than configured
    boost::asio::thread_pool th_pool(num_threads - 1);

    // Configuration for the connection pool
    boost::mysql::pool_params pool_prms{
        // Connect using TCP, to the given hostname and using the default port
        boost::mysql::host_and_port{mysql_hostname},

        // Authenticate using the given username
        mysql_username,

        // Password for the above username
        mysql_password,

        // Database to use when connecting
        "boost_mysql_examples",
    };

    // Create the connection pool
    auto shared_st = std::make_shared<shared_state>(boost::mysql::connection_pool(
        // Using thread_safe will create a strand for the connection pool.
        // This allows us to share the pool between sessions, which may run
        // concurrently, on different threads.
        boost::mysql::pool_executor_params::thread_safe(th_pool.get_executor()),

        // Pool config
        std::move(pool_prms)
    ));

    // A signal_set allows us to intercept SIGINT and SIGTERM and
    // exit gracefully
    boost::asio::signal_set signals{th_pool.get_executor(), SIGINT, SIGTERM};

    // Launch the MySQL pool
    shared_st->pool.async_run(boost::asio::detached);

    // Start listening for HTTP connections. This will run until the context is stopped
    auto ec = launch_server(th_pool.get_executor(), shared_st, port);
    if (ec)
    {
        log_error("Error launching server: ", ec);
        exit(EXIT_FAILURE);
    }

    // Capture SIGINT and SIGTERM to perform a clean shutdown
    signals.async_wait([shared_st, &th_pool](boost::system::error_code, int) {
        // Cancel the pool. This will cause async_run to complete.
        shared_st->pool.cancel();

        // Stop the execution context. This will cause main to exit
        th_pool.stop();
    });

    // Attach the current thread to the thread pool. This will block
    // until stop() is called
    th_pool.attach();

    // Wait until all threads have exited
    th_pool.join();

    std::cout << "Server exiting" << std::endl;

    // (If we get here, it means we got a SIGINT or SIGTERM)
    return EXIT_SUCCESS;
}
//
// File: types.hpp
//

#include <boost/core/span.hpp>
#include <boost/describe/class.hpp>
#include <boost/optional/optional.hpp>

#include <string>
#include <vector>

// Contains type definitions used in the REST API and database code.
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
// capabilities to our types. This allows using Boost.MySQL
// static interface (i.e. static_results<T>) to parse query results,
// and Boost.JSON automatic serialization/deserialization.

namespace notes {

struct note_t
{
    // The unique database ID of the object.
    std::int64_t id;

    // The note's title.
    std::string title;

    // The note's actual content.
    std::string content;
};
BOOST_DESCRIBE_STRUCT(note_t, (), (id, title, content))

//
// REST API requests.
//

// Used for creating and replacing notes
struct note_request_body
{
    // The title that the new note should have.
    std::string title;

    // The content that the new note should have.
    std::string content;
};
BOOST_DESCRIBE_STRUCT(note_request_body, (), (title, content))

//
// REST API responses.
//

// Used by endpoints returning several notes (like GET /notes).
struct multi_notes_response
{
    // The retrieved notes.
    std::vector<note_t> notes;
};
BOOST_DESCRIBE_STRUCT(multi_notes_response, (), (notes))

// Used by endpoints returning a single note (like GET /notes/<id>)
struct single_note_response
{
    // The retrieved note.
    note_t note;
};
BOOST_DESCRIBE_STRUCT(single_note_response, (), (note))

// Used by DELETE /notes/<id>
struct delete_note_response
{
    // true if the note was found and deleted, false if the note didn't exist.
    bool deleted;
};
BOOST_DESCRIBE_STRUCT(delete_note_response, (), (deleted))

}  // namespace notes
//
// File: repository.hpp
//

#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/string_view.hpp>

#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/optional/optional.hpp>

#include <cstdint>

#include "types.hpp"

namespace notes {

using boost::optional;
using boost::mysql::string_view;

// A lightweight wrapper around a connection_pool that allows
// creating, updating, retrieving and deleting notes in MySQL.
// This class encapsulates the database logic.
// All operations are async, and use stackful coroutines (boost::asio::yield_context).
// If the database can't be contacted, or unexpected database errors are found,
// an exception of type boost::mysql::error_with_diagnostics is thrown.
class note_repository
{
    boost::mysql::connection_pool& pool_;

public:
    // Constructor (this is a cheap-to-construct object)
    note_repository(boost::mysql::connection_pool& pool) : pool_(pool) {}

    // Retrieves all notes present in the database
    std::vector<note_t> get_notes(boost::asio::yield_context yield);

    // Retrieves a single note by ID. Returns an empty optional
    // if no note with the given ID is present in the database.
    optional<note_t> get_note(std::int64_t note_id, boost::asio::yield_context yield);

    // Creates a new note in the database with the given components.
    // Returns the newly created note, including the newly allocated ID.
    note_t create_note(string_view title, string_view content, boost::asio::yield_context yield);

    // Replaces the note identified by note_id, setting its components to the
    // ones passed. Returns the updated note. If no note with ID matching
    // note_id can be found, an empty optional is returned.
    optional<note_t> replace_note(
        std::int64_t note_id,
        string_view title,
        string_view content,
        boost::asio::yield_context yield
    );

    // Deletes the note identified by note_id. Returns true if
    // a matching note was deleted, false otherwise.
    bool delete_note(std::int64_t note_id, boost::asio::yield_context yield);
};

}  // namespace notes
//
// File: repository.cpp
//

#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/statement.hpp>
#include <boost/mysql/static_results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/throw_on_error.hpp>

#include <iterator>
#include <tuple>
#include <utility>

#include "repository.hpp"
#include "types.hpp"

using namespace notes;
namespace mysql = boost::mysql;

// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql
// The table looks like this:
//
// CREATE TABLE notes(
//     id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
//     title TEXT NOT NULL,
//     content TEXT NOT NULL
// );

std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
{
    mysql::diagnostics diag;
    mysql::error_code ec;

    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // Execute the query to retrieve all notes. We use the static interface to
    // parse results directly into static_results.
    mysql::static_results<note_t> result;
    conn->async_execute("SELECT id, title, content FROM notes", result, diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // By default, connections are reset after they are returned to the pool
    // (by using any_connection::async_reset_connection). This will reset any
    // session state we changed while we were using the connection
    // (e.g. it will deallocate any statements we prepared).
    // We did nothing to mutate session state, so we can tell the pool to skip
    // this step, providing a minor performance gain.
    // We use pooled_connection::return_without_reset to do this.
    conn.return_without_reset();

    // Move note_t objects into the result vector to save allocations
    return std::vector<note_t>(
        std::make_move_iterator(result.rows().begin()),
        std::make_move_iterator(result.rows().end())
    );
}

optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield)
{
    mysql::diagnostics diag;
    mysql::error_code ec;

    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // Our query has a parameter, so we need a prepared statement.
    // We don't need to deallocate this statement explicitly
    // (no need to call any_connection::async_close_statement).
    // The connection pool takes care of this for us
    // (by using any_connection::async_reset_connection).
    mysql::statement stmt = conn->async_prepare_statement(
        "SELECT id, title, content FROM notes WHERE id = ?",
        diag,
        yield[ec]
    );
    mysql::throw_on_error(ec, diag);

    // Execute the statement. We use the static interface to parse results
    mysql::static_results<note_t> result;
    conn->async_execute(stmt.bind(note_id), result, diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // An empty results object indicates that no note was found
    if (result.rows().empty())
        return {};
    else
        return std::move(result.rows()[0]);

    // There's no need to return the connection explicitly to the pool,
    // pooled_connection's destructor takes care of it.
}

note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield)
{
    mysql::diagnostics diag;
    mysql::error_code ec;

    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // Our query has parameters, so we need to prepare a statement.
    // As explained above, there is no need to deallocate the statement explicitly,
    // since the pool takes care of it.
    mysql::statement stmt = conn->async_prepare_statement(
        "INSERT INTO notes (title, content) VALUES (?, ?)",
        diag,
        yield[ec]
    );
    mysql::throw_on_error(ec, diag);

    // Execute the statement. The statement won't produce any rows,
    // so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> result;
    conn->async_execute(stmt.bind(title, content), result, diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // MySQL reports last_insert_id as a uint64_t regardless of the actual ID type.
    // Given our table definition, this cast is safe
    auto new_id = static_cast<std::int64_t>(result.last_insert_id());

    return note_t{new_id, title, content};

    // There's no need to return the connection explicitly to the pool,
    // pooled_connection's destructor takes care of it.
}

optional<note_t> note_repository::replace_note(
    std::int64_t note_id,
    string_view title,
    string_view content,
    boost::asio::yield_context yield
)
{
    mysql::diagnostics diag;
    mysql::error_code ec;

    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // Our query has parameters, so we need to prepare a statement.
    // As explained above, there is no need to deallocate the statement explicitly,
    // since the pool takes care of it.
    mysql::statement stmt = conn->async_prepare_statement(
        "UPDATE notes SET title = ?, content = ? WHERE id = ?",
        diag,
        yield[ec]
    );
    mysql::throw_on_error(ec, diag);

    // Execute the statement. The statement won't produce any rows,
    // so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> empty_result;
    conn->async_execute(stmt.bind(title, content, note_id), empty_result, diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // No affected rows means that the note doesn't exist
    if (empty_result.affected_rows() == 0u)
        return {};

    return note_t{note_id, title, content};

    // There's no need to return the connection explicitly to the pool,
    // pooled_connection's destructor takes care of it.
}

bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield)
{
    mysql::diagnostics diag;
    mysql::error_code ec;

    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // Our query has parameters, so we need to prepare a statement.
    // As explained above, there is no need to deallocate the statement explicitly,
    // since the pool takes care of it.
    mysql::statement stmt = conn->async_prepare_statement("DELETE FROM notes WHERE id = ?", diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // Execute the statement. The statement won't produce any rows,
    // so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> empty_result;
    conn->async_execute(stmt.bind(note_id), empty_result, diag, yield[ec]);
    mysql::throw_on_error(ec, diag);

    // No affected rows means that the note didn't exist
    return empty_result.affected_rows() != 0u;

    // There's no need to return the connection explicitly to the pool,
    // pooled_connection's destructor takes care of it.
}
//
// File: handle_request.hpp
//

#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>

#include "repository.hpp"

namespace notes {

// Handles an individual HTTP request, producing a response.
boost::beast::http::response<boost::beast::http::string_body> handle_request(
    const boost::beast::http::request<boost::beast::http::string_body>& request,
    note_repository repo,
    boost::asio::yield_context yield
);

}  // namespace notes
//
// File: handle_request.cpp
//

#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>

#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <boost/optional/optional.hpp>
#include <boost/url/parse.hpp>
#include <boost/variant2/variant.hpp>

#include <cstdint>
#include <exception>
#include <iostream>
#include <string>

#include "handle_request.hpp"
#include "repository.hpp"
#include "types.hpp"

// This file contains all the boilerplate code to dispatch HTTP
// requests to API endpoints. Functions here end up calling
// note_repository fuctions.

namespace asio = boost::asio;
namespace http = boost::beast::http;
using boost::mysql::error_code;
using boost::mysql::string_view;
using namespace notes;

namespace {

// Attempts to parse a numeric ID from a string.
// If you're using C++17, you can use std::from_chars, instead
static boost::optional<std::int64_t> parse_id(const std::string& from)
{
    try
    {
        std::size_t consumed = 0;
        int res = std::stoi(from, &consumed);
        if (consumed != from.size())
            return {};
        else if (res < 0)
            return {};
        return res;
    }
    catch (const std::exception&)
    {
        return {};
    }
}

// Encapsulates the logic required to match a HTTP request
// to an API endpoint, call the relevant note_repository function,
// and return an HTTP response.
class request_handler
{
    // The HTTP request we're handling. Requests are small in size,
    // so we use http::request<http::string_body>
    const http::request<http::string_body>& request_;

    // The repository to access MySQL
    note_repository repo_;

    // Creates an error response
    http::response<http::string_body> error_response(http::status code, string_view msg) const
    {
        http::response<http::string_body> res;

        // Set the status code
        res.result(code);

        // Set the keep alive option
        res.keep_alive(request_.keep_alive());

        // Set the body
        res.body() = msg;

        // Adjust the content-length field
        res.prepare_payload();

        // Done
        return res;
    }

    // Used when the request's Content-Type header doesn't match what we expect
    http::response<http::string_body> invalid_content_type() const
    {
        return error_response(http::status::bad_request, "Invalid content-type");
    }

    // Used when the request body didn't match the format we expect
    http::response<http::string_body> invalid_body() const
    {
        return error_response(http::status::bad_request, "Invalid body");
    }

    // Used when the request's method didn't match the ones allowed by the endpoint
    http::response<http::string_body> method_not_allowed() const
    {
        return error_response(http::status::method_not_allowed, "Method not allowed");
    }

    // Used when the request target couldn't be matched to any API endpoint
    http::response<http::string_body> endpoint_not_found() const
    {
        return error_response(http::status::not_found, "The requested resource was not found");
    }

    // Used when the user requested a note (e.g. using GET /note/<id> or PUT /note/<id>)
    // but the note doesn't exist
    http::response<http::string_body> note_not_found() const
    {
        return error_response(http::status::not_found, "The requested note was not found");
    }

    // Creates a response with a serialized JSON body.
    // T should be a type with Boost.Describe metadata containing the
    // body data to be serialized
    template <class T>
    http::response<http::string_body> json_response(const T& body) const
    {
        http::response<http::string_body> res;

        // A JSON response is always a 200
        res.result(http::status::ok);

        // Set the content-type header
        res.set("Content-Type", "application/json");

        // Set the keep-alive option
        res.keep_alive(request_.keep_alive());

        // Serialize the body data into a string and use it as the response body.
        // We use Boost.JSON's automatic serialization feature, which uses Boost.Describe
        // reflection data to generate a serialization function for us.
        res.body() = boost::json::serialize(boost::json::value_from(body));

        // Adjust the content-length header
        res.prepare_payload();

        // Done
        return res;
    }

    // Returns true if the request's Content-Type is set to JSON
    bool has_json_content_type() const
    {
        auto it = request_.find("Content-Type");
        return it != request_.end() && it->value() == "application/json";
    }

    // Attempts to parse the request body as a JSON into an object of type T.
    // T should be a type with Boost.Describe metadata.
    // We use boost::system::result, which may contain a result or an error.
    template <class T>
    boost::system::result<T> parse_json_request() const
    {
        error_code ec;

        // Attempt to parse the request into a json::value.
        // This will fail if the provided body isn't valid JSON.
        auto val = boost::json::parse(request_.body(), ec);
        if (ec)
            return ec;

        // Attempt to parse the json::value into a T. This will
        // fail if the provided JSON doesn't match T's shape.
        return boost::json::try_value_to<T>(val);
    }

    http::response<http::string_body> handle_request_impl(boost::asio::yield_context yield)
    {
        // Parse the request target. We use Boost.Url to do this.
        auto url = boost::urls::parse_origin_form(request_.target());
        if (url.has_error())
            return error_response(http::status::bad_request, "Invalid request target");

        // We will be iterating over the target's segments to determine
        // which endpoint we are being requested
        auto segs = url->segments();
        auto segit = segs.begin();
        auto seg = *segit++;

        // All endpoints start with /notes
        if (seg != "notes")
            return endpoint_not_found();

        if (segit == segs.end())
        {
            if (request_.method() == http::verb::get)
            {
                // GET /notes: retrieves all the notes.
                // The request doesn't have a body.
                // The response has a JSON body with multi_notes_response format
                auto res = repo_.get_notes(yield);
                return json_response(multi_notes_response{std::move(res)});
            }
            else if (request_.method() == http::verb::post)
            {
                // POST /notes: creates a note.
                // The request has a JSON body with note_request_body format.
                // The response has a JSON body with single_note_response format.

                // Parse the request body
                if (!has_json_content_type())
                    return invalid_content_type();
                auto args = parse_json_request<note_request_body>();
                if (args.has_error())
                    return invalid_body();

                // Actually create the note
                auto res = repo_.create_note(args->title, args->content, yield);

                // Return the newly crated note as response
                return json_response(single_note_response{std::move(res)});
            }
            else
            {
                return method_not_allowed();
            }
        }
        else
        {
            // The URL has the form /notes/<note-id>. Parse the note ID.
            auto note_id = parse_id(*segit++);
            if (!note_id.has_value())
            {
                return error_response(
                    http::status::bad_request,
                    "Invalid note_id specified in request target"
                );
            }

            // /notes/<note-id>/<something-else> is not a valid endpoint
            if (segit != segs.end())
                return endpoint_not_found();

            if (request_.method() == http::verb::get)
            {
                // GET /notes/<note-id>: retrieves a single note.
                // The request doesn't have a body.
                // The response has a JSON body with single_note_response format

                // Get the note
                auto res = repo_.get_note(*note_id, yield);

                // If we didn't find it, return a 404 error
                if (!res.has_value())
                    return note_not_found();

                // Return it as response
                return json_response(single_note_response{std::move(*res)});
            }
            else if (request_.method() == http::verb::put)
            {
                // PUT /notes/<note-id>: replaces a note.
                // The request has a JSON body with note_request_body format.
                // The response has a JSON body with single_note_response format.

                // Parse the JSON body
                if (!has_json_content_type())
                    return invalid_content_type();
                auto args = parse_json_request<note_request_body>();
                if (args.has_error())
                    return invalid_body();

                // Perform the update
                auto res = repo_.replace_note(*note_id, args->title, args->content, yield);

                // Check that it took effect. Otherwise, it's because the note wasn't there
                if (!res.has_value())
                    return note_not_found();

                // Return the updated note as response
                return json_response(single_note_response{std::move(*res)});
            }
            else if (request_.method() == http::verb::delete_)
            {
                // DELETE /notes/<note-id>: deletes a note.
                // The request doesn't have a body.
                // The response has a JSON body with delete_note_response format.

                // Attempt to delete the note
                bool deleted = repo_.delete_note(*note_id, yield);

                // Return whether the delete was successful in the response.
                // We don't fail DELETEs for notes that don't exist.
                return json_response(delete_note_response{deleted});
            }
            else
            {
                return method_not_allowed();
            }
        }
    }

public:
    // Constructor
    request_handler(const http::request<http::string_body>& req, note_repository repo)
        : request_(req), repo_(repo)
    {
    }

    // Generates a response for the request passed to the constructor
    http::response<http::string_body> handle_request(boost::asio::yield_context yield)
    {
        try
        {
            // Attempt to handle the request
            return handle_request_impl(yield);
        }
        catch (const boost::mysql::error_with_diagnostics& err)
        {
            // A Boost.MySQL error. This will happen if you don't have connectivity
            // to your database, your schema is incorrect or your credentials are invalid.
            // Log the error, including diagnostics, and return a generic 500
            log_error(
                "Uncaught exception: ",
                err.what(),
                "\nServer diagnostics: ",
                err.get_diagnostics().server_message()
            );
            return error_response(http::status::internal_server_error, "Internal error");
        }
        catch (const std::exception& err)
        {
            // Another kind of error. This indicates a programming error or a severe
            // server condition (e.g. out of memory). Same procedure as above.
            log_error("Uncaught exception: ", err.what());
            return error_response(http::status::internal_server_error, "Internal error");
        }
    }
};

}  // namespace

// External interface
boost::beast::http::response<boost::beast::http::string_body> notes::handle_request(
    const boost::beast::http::request<boost::beast::http::string_body>& request,
    note_repository repo,
    boost::asio::yield_context yield
)
{
    return request_handler(request, repo).handle_request(yield);
}
//
// File: server.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/system/error_code.hpp>

#include <memory>

namespace notes {

// State shared by all sessions created by our server.
// For this application, we only need a connection_pool object.
// Place here any other singleton objects your application may need.
// We will use std::shared_ptr<shared_state> to ensure that objects
// are kept alive until all sessions are terminated.
struct shared_state
{
    boost::mysql::connection_pool pool;

    shared_state(boost::mysql::connection_pool pool) : pool(std::move(pool)) {}
};

// Launches a HTTP server that will listen on 0.0.0.0:port.
// If the server fails to launch (e.g. because the port is aleady in use),
// returns a non-zero error code. ex should identify the io_context or thread_pool
// where the server should run. The server is run until the underlying execution
// context is stopped.
boost::system::error_code launch_server(
    boost::asio::any_io_executor ex,
    std::shared_ptr<shared_state> state,
    unsigned short port
);

}  // namespace notes
//
// File: server.cpp
//

#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/error_code.hpp>

#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/parser.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/write.hpp>

#include <cstddef>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
#include <string>

#include "handle_request.hpp"
#include "repository.hpp"
#include "server.hpp"
#include "types.hpp"

// This file contains all the boilerplate code to implement a HTTP
// server. Functions here end up invoking handle_request.

namespace asio = boost::asio;
namespace http = boost::beast::http;
using boost::mysql::error_code;
using namespace notes;

namespace {

static void run_http_session(
    boost::asio::ip::tcp::socket sock,
    std::shared_ptr<shared_state> st,
    boost::asio::yield_context yield
)
{
    error_code ec;

    // A buffer to read incoming client requests
    boost::beast::flat_buffer buff;

    while (true)
    {
        // Construct a new parser for each message
        http::request_parser<http::string_body> parser;

        // Apply a reasonable limit to the allowed size
        // of the body in bytes to prevent abuse.
        parser.body_limit(10000);

        // Read a request
        http::async_read(sock, buff, parser.get(), yield[ec]);

        if (ec)
        {
            if (ec == http::error::end_of_stream)
            {
                // This means they closed the connection
                sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
            }
            else
            {
                // An unknown error happened
                log_error("Error reading HTTP request: ", ec);
            }
            return;
        }

        // Process the request to generate a response.
        // This invokes the business logic, which will need to access MySQL data
        auto response = handle_request(parser.get(), note_repository(st->pool), yield);

        // Determine if we should close the connection
        bool keep_alive = response.keep_alive();

        // Send the response
        http::async_write(sock, response, yield[ec]);
        if (ec)
            return log_error("Error writing HTTP response: ", ec);

        // This means we should close the connection, usually because
        // the response indicated the "Connection: close" semantic.
        if (!keep_alive)
        {
            sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
            return;
        }
    }
}

// Implements the server's accept loop. The server will
// listen for connections until stopped.
static void do_accept(
    asio::any_io_executor executor,  // The original executor (without strands)
    std::shared_ptr<asio::ip::tcp::acceptor> acceptor,
    std::shared_ptr<shared_state> st
)
{
    acceptor->async_accept([executor, st, acceptor](error_code ec, asio::ip::tcp::socket sock) {
        // If there was an error accepting the connection, exit our loop
        if (ec)
            return log_error("Error while accepting connection", ec);

        // Launch a new session for this connection. Each session gets its
        // own stackful coroutine, so we can get back to listening for new connections.
        boost::asio::spawn(
            // Every session gets its own strand. This prevents data races.
            asio::make_strand(executor),

            // The actual coroutine
            [st, socket = std::move(sock)](boost::asio::yield_context yield) mutable {
                run_http_session(std::move(socket), std::move(st), yield);
            },

            // All errors in the session are handled via error codes or by catching
            // exceptions explicitly. An unhandled exception here means an error.
            // Rethrowing it will propagate the exception, making io_context::run()
            // to throw and terminate the program.
            [](std::exception_ptr ex) {
                if (ex)
                    std::rethrow_exception(ex);
            }
        );

        // Accept a new connection
        do_accept(executor, acceptor, st);
    });
}

}  // namespace

error_code notes::launch_server(
    boost::asio::any_io_executor ex,
    std::shared_ptr<shared_state> st,
    unsigned short port
)
{
    error_code ec;

    // An object that allows us to acept incoming TCP connections.
    // Since we're in a multi-threaded environment, we create a strand for the acceptor,
    // so all accept handlers are run serialized
    auto acceptor = std::make_shared<asio::ip::tcp::acceptor>(asio::make_strand(ex));

    // The endpoint where the server will listen. Edit this if you want to
    // change the address or port we bind to.
    boost::asio::ip::tcp::endpoint listening_endpoint(boost::asio::ip::make_address("0.0.0.0"), port);

    // Open the acceptor
    acceptor->open(listening_endpoint.protocol(), ec);
    if (ec)
        return ec;

    // Allow address reuse
    acceptor->set_option(asio::socket_base::reuse_address(true), ec);
    if (ec)
        return ec;

    // Bind to the server address
    acceptor->bind(listening_endpoint, ec);
    if (ec)
        return ec;

    // Start listening for connections
    acceptor->listen(asio::socket_base::max_listen_connections, ec);
    if (ec)
        return ec;

    std::cout << "Server listening at " << acceptor->local_endpoint() << std::endl;

    // Launch the acceptor loop
    do_accept(std::move(ex), std::move(acceptor), std::move(st));

    // Done
    return error_code();
}
//
// File: log_error.hpp
//

#include <iostream>
#include <mutex>

// Helper function to safely write diagnostics to std::cerr.
// Since we're in a multi-threaded environment, directly writing to std::cerr
// can lead to interleaved output, so we should synchronize calls with a mutex.
// This function is only called in rare cases (e.g. unhandled exceptions),
// so we can afford the synchronization overhead.

namespace notes {

// If you're in C++17+, you can write this using fold expressions
// instead of recursion.
inline void log_error_impl() {}

template <class Arg1, class... Tail>
void log_error_impl(const Arg1& arg, const Tail&... tail)
{
    std::cerr << arg;
    log_error_impl(tail...);
}

template <class... Args>
void log_error(const Args&... args)
{
    static std::mutex mtx;

    // Acquire the mutex, then write the passed arguments to std::cerr.
    std::unique_lock<std::mutex> lock(mtx);
    log_error_impl(args...);
    std::cerr << std::endl;
}

}  // namespace notes

PrevUpHomeNext