Skip to content

CXX-3082 Add comprehensive examples (mongocxx) #1216

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

Merged
merged 31 commits into from
Oct 10, 2024

Conversation

eramongodb
Copy link
Contributor

@eramongodb eramongodb commented Oct 1, 2024

Summary

Followup to #1208. Partially resolves CXX-3082. Verified by this patch.

As before, API docs must be generated locally for review in their displayed state.

Adds comprehensive examples for most mongocxx interfaces. Due to the size of mongocxx (this PR is already the same size as the PR for bsoncxx despite being incomplete), this PR is posted as a Part 1? for mongocxx to allow for pipelining changes for a followup PR during review of this PR.

Examples for the following are planned but not yet ready for review (pipelined):

  • Databases Error Handling (that are not operation exceptions)
  • Collections Error Handling (that are not operation exceptions)
  • Change Streams
  • Sessions
  • Transactions

Examples for the following are not planned and are deliberately skipped:

  • CSFLE
  • GridFS Buckets
  • Range Indexes
  • Search Indexes

See "Regarding Skipped Examples" below for more detail.

API Examples Runner

To accomodate the mongocxx library's use of mongocxx::instance for global initialization and cleanup, the API examples runner requires some additional features not present in #1208.

There are now three different "kinds" of components:

  • Normal: does not require a mongocxx instance (i.e. bsoncxx API examples).
  • Forking: requires the component to be executed in its own process. This is used by mongocxx::instance examples that create-and-destroy their own mongocxx instance objects.
  • With Instance: requires a mongocxx instance, but does not create or destroy one itself (most mongocxx examples).
    • For Server: a subset of "with instance" components that require a live connection to a MongoDB server.

"Normal" components are executed in the same way they were in #1208 (with some minor improvements to support reuse + handle --jobs 1 better).

"Forking" components execute each component in its own forked process. This permits creating and destroying a mongocxx::instance object in each forking component without affecting other components (since only one instance object is allowed per application). When fork() is not available (specifically, with MSVC), these components are skipped entirely.

"With Instance" components execute each component just like normal components, but are executed after creating a single mongocxx::instance object. This instance object is shared across all subsequently executed components such that they do not require repeatedly invoking mongocxx::instance::current() to handle indeterminate execution ordering (as is currently done in the test suite).

Important

"With Instance" components must be executed after executing forking components. Otherwise, forking components will observe an instance object was already created before the fork takes place.

"For Server" components are a subset of "With Instance" components that are only executed if a live server is available. To maximize coverage, components that require a single server topology are also executed against replica and sharded topologies, and components that require a replica set are also executed against sharded topologies (coverage size: Single > Replica > Sharded). This behavior can be changed or removed if deemed unnecessary.

Note

Currently, all examples only require a single server topology. Examples which depend on a specific topology are likely unnecessary and are better left to the test suite.

DB Locker

The API examples are deliberately executed concurrently (for performance) and in a random order (for independence). To avoid examples that operate on a given database from conflicting with one another, the db_lock utility is used to guard access to a given database by name. This is implemented as a db_name -> db_mutex map. The access of the map itself also requires a mutex that is held only for as long as is required to obtain a db_mutex lock.

{
  db_lock guard{client, "db"}; // Guard access to "db" for purpose of this example.
  example(client);
}
void example(mongocxx::client& client) {
  auto db = client["db"]; // Use in example code block without concern.
  // ...
}

Note

The high lock contention of the map mutex is not expected to be a significant problem for the purpose of executing API examples, as most time will likely be spent locking a db_mutex and running the example code block. db_mutex locks for components using unique database names could be elided, but this was deliberately not done to keep things simple and easier to maintain. Performance is not the priority here.

This permits examples to use common names in examples (i.e. client["db"], db["coll"]) without concern for concurrency database access problems. When not required by the example code block, the database name can be made unique to the component via EXAMPLES_COMPONENT_NAME_STR. This helps avoid unnecessary db_mutex lock contention.

{
  db_lock guard{client, EXAMPLES_COMPONENT_NAME_STR}; // Unique database name for this example.
  example(guard.get());
}
// Example code does not need to reveal the name of the database.
void example(mongocxx::database db) {
  auto coll = db["coll"]; // Use in example code block without concern.
  // ...
}

For convenience, db_lock drops the database on lock and unlock to ensure every example component is given a clean slate to work with. This permits easily setting up collections and documents for use by the example code block.

Read/Write Concern

To ensure examples behave correctly regardless of server topology, a convenience function set_rw_concern_majority() is used to set the read concern and write concern for databases and collections to "majority". This ensures any examples that expect writes to be reliably observed by subsequent reads will work even for replica sets and sharded clusters without requiring boilerplate in the example code block.

Writing mongocxx Examples

Given the large scope of interfaces and overloads in mongocxx, some rough rules are used to avoid redundancy and verbosity of examples:

  • A "representative example" is used for function overloads and class types that fulfill a similar purpose using a similar code pattern, e.g. bulk write models, APM callbacks, etc. (author's judgement). The API documentation itself may be used to translate and extend representative examples to related types/overloads.
  • Some examples return a value from the example code block for further correctness validation that is not required by the example code block itself. This is an extension of the parameter(s) used by example code block and is not expected to impact the readability of examples for regular users.
  • "Inline" code (e.g. temporary objects, method chaining, etc.) and auto are usually avoided in example code to aid in interface clarity, unless it is not directly related to the primary function/task itself. This is best observed in example code blocks that have both a "Basic Usage" block (little-to-no inline code) and a subsequent "With Options" block (inline code for already-demonstrated routines), as well as for bsoncxx interface usage in mongocxx example code blocks.

Error handling for databases and collections for interfaces that may throw a mongocxx::operation_exception exception have been summarized in a dedicated "Operation Exceptions" page. The page also includes an important warning regarding mongocxx::server_error_category() deficiencies (overloaded error codes: CXX-834) as well as a recommendation to use mongocxx::exception instead for forward-compatibility (error handling redesign: CXX-2377).

Regarding Skipped Examples

  • CSFLE: despite high value for users, writing even "simple" examples for these interfaces is incredibly difficult. I have deliberately elected to skip these examples in the interest of time and prioritizing other tasks.
    • The error code and exception hierarchy situation (CXX-2377) is hurting CSFLE interfaces the most: an error code thrown by a single function may come from the server, libmongoc, or libmongocrypt, with no means of reliably discerning them from one another (see: mongocxx/examples/clients/create/single/options/auto_encryption.cpp). 😢
  • GridFS Buckets: low priority / low value to users?
  • Range Indexes: requires Atlas. Examples may need to be unverified (not executed) if written.
  • Search Indexes: requires Atlas. Examples may need to be unverified (not executed) if written.

These examples may be reconsidered for inclusion if their value to users is sufficiently high.

Miscellaneous

  • Added missing docs for mongocxx::v_noabi::uri::k_default_uri (was not referencable by Doxygen).
  • Added a temporary workaround for CDRIVER-5732 in "big string" examples.
  • Added an uncaught exception handler for components to make identifying the failing component easier when executed as a thread.
  • Runs components in current thread rather than per-thread give --jobs 1 for simplicity and improved debugging experience.

@eramongodb eramongodb requested a review from kevinAlbs October 1, 2024 20:04
@eramongodb eramongodb self-assigned this Oct 1, 2024
@kevinAlbs kevinAlbs requested a review from adriandole October 1, 2024 20:10
@eramongodb
Copy link
Contributor Author

Synced with #1230 (rename ASSERT -> EXPECT).

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 suggestions. I like the database lock idea to safely parallelize.

EXPECT(!client);

try {
mongocxx::uri uri = client.uri(); // DO NOT DO THIS. Throws.
Copy link
Contributor

Choose a reason for hiding this comment

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

Strong language, is this worse than the other examples that throw exceptions (uninitialized read or something)? If so it should be stated, if not just the regular 'throws' comment is enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The pattern of an "invalid" or "empty" object (be it clients, iterators, elements, etc.) is explicitly discouraged in example code whose purpose is to demonstrate the recoverable error should the user violate the same principles behind !ptr -> use(*ptr) or !opt -> opt->use(). The purpose of these error handling examples is to demonstrate how to handle the error, not to encourage depending on it.


EXPECT(hold);

mongocxx::pool::entry entry = pool.acquire(); // Throws.
Copy link
Contributor

@adriandole adriandole Oct 9, 2024

Choose a reason for hiding this comment

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

Point out that this doesn't work because of maxPoolSize=1 in the URI. If you read a lot of these you start to mentally skip over the URIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The purpose of this error handling example is to demonstrate how to handle an exception thrown by a wait queue timeout. The explanation of the conditions used to trigger the timeout are documented by relevant reference documentation.

// Use mongocxx library interfaces at this point.
use(mongocxx::client{});

// Cleanup the MongoDB C++ Driver.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Cleanup the MongoDB C++ Driver.
// Destroying the `instance` cleans up the MongoDB C++ Driver.

Make explicit that it's the instance destructor as opposed to the client.

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 the wording as suggested to make implicit-syntax behavior clearer.


// [Example]
void example() {
{ mongocxx::instance instance; } // Initialize and cleanup.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
{ mongocxx::instance instance; } // Initialize and cleanup.
{ mongocxx::instance instance; } // Initialize and cleanup. May be done only once per program lifetime.

The example is clear but this rule should be explicitly stated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The rule is explicitly stated in the reference documentation for the instance object. Per Diataxis, these example code blocks deliberately refrain from "digression, explanation, [or] teaching":

Typically, the temptations are to explain or to provide reference for completeness. Neither of these are part of guiding the user in their work. They get in the way of the action; if they’re important, link to them.

A how-to guide serves the work of the already-competent user, whom you can assume to know what they want to do, and to be able to follow your instructions correctly.

Clarifications by inline comments are reserved for highlighting essential behavior or unintuitive, easy-to-miss aspects of the example being demonstrated. For reference, see also the BSON document error handling examples added by #1208.

@eramongodb eramongodb requested a review from adriandole October 10, 2024 14:59
@eramongodb eramongodb merged commit d7780dc into mongodb:master Oct 10, 2024
66 of 73 checks passed
@eramongodb eramongodb deleted the cxx-3082 branch October 10, 2024 19:31
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