Skip to content

Conversation

eramongodb
Copy link
Contributor

@eramongodb eramongodb commented Sep 26, 2025

Related to CXX-3320. Due to the unique nature of mongocxx::v_noabi::instance as a global singleton-like object, this class is migrated in full from v_noabi to v1 as a single, standalone PR. Related types v_noabi::logger and v_noabi::exception are also minimally migrated as part of this PR to support the instance class API.


First, concerning v_noabi::logger: due to having no observable difference in behavior in v1, v_noabi::logger is merely a redeclaration of v1::logger in the v_noabi namespace! 🎉 This also avoids complications with trying to support compatibility between both a v_noabi::logger and v1::logger class given their polymorphic nature.


The v_noabi::instance class is not so fortunate. Its migration is complicated primary by the realization that there are two unresolvable deficiencies with the v_noabi::instance API:

  • it does not support using the mongoc default log handler, and
  • the custom log handler (if any) is registered after mongoc_init() has already been invoked.

As mentioned by mongoc docs:

Note that in the example above mongoc_log_set_handler() is called before mongoc_init(). Otherwise, some log traces could not be processed by the log handler.

That is, the initialization behavior is currently defined as follows (pseudocode):

void v_noabi::init(mongoc_log_func_t handler) {
    mongoc_init(); // May emit log messages!

    if (handler) {
        mongoc_log_set_handler(handler); // Already initialized!
    } else {
        mongoc_log_set_handler(nullptr); // Disable log messages!
    }

    // ... handshake ...
}

The v1::instance object proposes fixing these issues as follows:

  • Support a v1::default_handler tag type by which the user can request using the mongoc default log handler.
  • Register or disable the custom log handler before calling mongoc_init() so that all log messages are handled properly.

That is, the initialization behavior is changed to the following:

void v1::init(mongoc_log_func_t handler) {
    v1::init::impl(handler, true); // Set a custom log handler.
}

void v1::init(default_handler /* tag */) {
    v1::init::impl(nullptr, false); // Use default log handler.
}

void v1::init::impl(mongoc_log_func_t handler, bool set_custom_handler) {
    // Support using the default log handler.
    if (set_custom_handler) {
        if (handler) {
            mongoc_log_set_handler(handler); // Already initialized!
        } else {
            mongoc_log_set_handler(nullptr); // Disable log messages!
        }
    }

    mongoc_init(); // All log messages are captured.

    // ... handshake ...
}

For backward compatibility, v_noabi::instance's custom log handler constructor preserves the "register handler after mongoc_init()" behavior. If we do not consider this behavior to be worth preserving (how important are log messages which may be emitted by mongoc_init()?), we can simplify the implementation of v_noabi::instance's initialization. (Related: CXX-1029) Update: per feedback, simplified the v_noabi implementation to also register the custom unstructured log handler before mongoc initialization.

The v_noabi::instance still supports the (deprecated) ::instance() helper function for backward compatibility. The implementation of ::instance() remains specific to v_noabi: the implementation v1::instance is entirely unaware of ::instance() support. In place of the current_instance "sentinel" value, v1::instance instead utilizes a simple std::atomic_int counter to track and report exceptions given multiple instance objects.


Much of the rest of this PR involves refactors to the test suite for the instance and logger classes. The test_logging executable is removed in favor of grouping all instance object testing to test_instance. All other test executables can (in theory) be grouped into a single test executable without concern for "multiple instance object" exceptions.

The fork-based pattern to test instance objects used by the examples API runner (#1216) is repurposed here for compatibility with the Catch2 test suite as mongocxx::test::subprocess(op). This helper function accepts a single op: void() invocable that is executed within a forked subprocess. The result of the subprocess, including termination, is translated into well-behaved Catch test assertions. The Catch test suite reporter is reused by the subprocess to support capturing values and evaluating assertions. However, using SECTION() is not supported due to non-trivial control flow, either within op or in the parent TEST_CASE().

This enabled writing test cases asserting proper log message handling behavior, including default log message handler behavior, by capturing log output emitted by the subprocess (using POSIX pipe API in the capture_stderr class). Note this is only supported on non-Windows platforms. Unfortunately, the use of std::exit() and std::terminate() in the subprocess confuses valgrind, which (understandably) reports memory leaks due to the abnormal process termination method. Therefore, leak detection in subprocesses is disabled for the test_instance executable using --trace-children=no.

@eramongodb eramongodb self-assigned this Sep 26, 2025
@eramongodb eramongodb requested a review from a team as a code owner September 26, 2025 19:21
instance& operator=(instance const&) = delete;

///
/// Initialize the mongoc library with unstructured log messages disabled.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated documentation to clarify that this all applies to "unstructured log handling" to leave open the design space for "structured log handling" support by the C++ Driver.

Documentation also increased references to specific mongoc API to clarify behavior + defer much of the behavioral documentation to mongoc.

Comment on lines +79 to +97
///
/// This class is not moveable.
///
instance(instance&&) = delete;

///
/// This class is not moveable.
///
instance& operator=(instance&&) = delete;

///
/// This class is not copyable.
///
instance(instance const&) = delete;

///
/// This class is not copyable.
///
instance& operator=(instance const&) = delete;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relative breaking change: instance is made immovable as well as uncopyable. Allowing instance to be moved-from does not seem to be well-motivated.

mongoc_log_level_t log_level,
char const* domain,
char const* message,
void* user_data) noexcept {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small relative change: added noexcept for consistency with other C API callback functions.

Comment on lines +126 to +128
"mongocxx",
MONGOCXX_VERSION_STRING,
"CXX=" MONGOCXX_COMPILER_ID " " MONGOCXX_COMPILER_VERSION " stdcxx=" STDCXX " / ");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor optimization: avoid allocations, given all fields are literals, by concatenating everything into a single string literal.

// Use `std::exit()` and `std::terminate()` to prevent continued execution of the Catch2 test suite.
try {
fn();
std::exit(EXIT_SUCCESS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked that these uses of exit are correct in the child process? Usually, with a fork() without exec(), you want to use _Exit instead of exit() to prevent cleanup handlers from executing in the weird intermediate state of the child process. (Using _Exit may or may not also avoid Valgrind weirdness.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, with a fork() without exec(), you want to use _Exit instead of exit()

Good call. Converted std::exit() into std::_Exit() within the subprocess.

Using _Exit may or may not also avoid Valgrind weirdness.

Alas, this doesn't entirely address Valgrind weirdness, as the abnormal termination of a subprocess still produces noisy diagnostic messages due to "still reachable" allocations in its exit summary (one for each terminated subprocess). There doesn't seem to be a straightforward way to suppress the diagnostic messages of the subprocesses only while still performing at-exit leak checks. Given the limited use of this pattern, I think I would prefer defer memchecking of subprocesses to ASAN/UBSAN task coverage instead and keep the --trace-children=no Valgrind flag for test_instance (fwiw it doesn't apply to any other test executables).

Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with minor comments. I like the new overload to simplify enabling default logging.

how important are log messages which may be emitted by mongoc_init()?

I see two possible logs:

  • 1 Failure to initialize OpenSSL.
  • 2 Failure to get OS version for handshake.

I expect neither is likely. (2) might be improved by making handshake non-global (CDRIVER-4142) (errors could be returned on the first client operation).

For backward compatibility, v_noabi::instance's custom log handler constructor preserves the "register handler after mongoc_init()" behavior.

If it helps simplify: I expect having v_noabi register the log handler before mongoc_init (to match v1) is unlikely to harm consumers (some added unlikely-to-occur logs)

Comment on lines 32 to 33
// @returns The exit code of the subprocess (`*is_signal` is `false`) or the signal used to kill the subprocess
// (`*is_signal` is `true`) .
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// @returns The exit code of the subprocess (`*is_signal` is `false`) or the signal used to kill the subprocess
// (`*is_signal` is `true`) .
// @returns The exit code of the subprocess (`*is_signal_ptr` is `false`) or the signal used to kill the subprocess
// (`*is_signal_ptr` is `true`) .

Comment on lines 143 to 149
CHECK_SUBPROCESS([] {
// Try to silence noisy Catch2 output.
(void)::close(1); // stdout
(void)::close(2); // stderr

SKIP("subprocess");
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CHECK_SUBPROCESS([] {
// Try to silence noisy Catch2 output.
(void)::close(1); // stdout
(void)::close(2); // stderr
SKIP("subprocess");
});
CHECK_SUBPROCESS([] {
// Try to silence noisy Catch2 output.
(void)::close(1); // stdout
(void)::close(2); // stderr
SKIP("subprocess");
}, &is_signal);


} // namespace

// mongocxx::v1::logger must not unnecessarily mpose special requirements on derived classes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// mongocxx::v1::logger must not unnecessarily mpose special requirements on derived classes.
// mongocxx::v1::logger must not unnecessarily impose special requirements on derived classes.

///
/// Cleanup the mongocxx (and mongoc) library.
///
/// Calls [`mongoc_init()`](https://mongoc.org/libmongoc/current/mongoc_cleanup.html).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Calls [`mongoc_init()`](https://mongoc.org/libmongoc/current/mongoc_cleanup.html).
/// Calls [`mongoc_cleanup()`](https://mongoc.org/libmongoc/current/mongoc_cleanup.html).

Comment on lines 40 to 41
static_assert(!std::is_move_constructible<instance>::value, "bsoncxx::v1::instance must be non-moveable");
static_assert(!std::is_copy_constructible<instance>::value, "bsoncxx::v1::instance must be non-copyable");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static_assert(!std::is_move_constructible<instance>::value, "bsoncxx::v1::instance must be non-moveable");
static_assert(!std::is_copy_constructible<instance>::value, "bsoncxx::v1::instance must be non-copyable");
static_assert(!std::is_move_constructible<instance>::value, "mongocxx::v1::instance must be non-moveable");
static_assert(!std::is_copy_constructible<instance>::value, "mongocxx::v1::instance must be non-copyable");

Copy link
Contributor Author

@eramongodb eramongodb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it helps simplify: I expect having v_noabi register the log handler before mongoc_init (to match v1) is unlikely to harm consumers (some added unlikely-to-occur logs)

Simplified the v_noabi implementation as suggested.


// Inform the user that a custom log handler has been registered.
// Cannot use mocked `libmongoc::log()` due to varargs.
mongoc_log(MONGOC_LOG_LEVEL_INFO, "mongocxx", "libmongoc logging callback enabled");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to keep this informational log message?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am slightly in favor of keeping for v_noabi. The API examples expect it, and it might avoid a surprise to consumers. I expect it is not much burden for us to keep it in v_noabi. But I do not think it is needed for v1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants