https://github.com/siddiqsoft/rwlenvelope
Read-write envelope class
https://github.com/siddiqsoft/rwlenvelope
cpp17 lock locking nuget reader-writer thread-safe utility-class utility-classes
Last synced: 12 days ago
JSON representation
Read-write envelope class
- Host: GitHub
- URL: https://github.com/siddiqsoft/rwlenvelope
- Owner: SiddiqSoft
- License: bsd-3-clause
- Created: 2021-07-22T06:54:04.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2026-04-29T21:28:23.000Z (about 1 month ago)
- Last Synced: 2026-04-29T22:31:03.102Z (about 1 month ago)
- Topics: cpp17, lock, locking, nuget, reader-writer, thread-safe, utility-class, utility-classes
- Language: C++
- Homepage:
- Size: 106 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
RWLEnvelope : A simple read-writer lock envelope
-------------------------------------------
[](https://github.com/SiddiqSoft/RWLEnvelope/actions/workflows/codeql-analysis.yml)
[](https://dev.azure.com/siddiqsoft/siddiqsoft/_build/latest?definitionId=7&branchName=main)




## Objective
**RWLEnvelope** is a header-only C++ template library that provides a simple, convenient envelope-access model for thread-safe access to objects using reader-writer locks. Our goal is to:
- **Avoid re-implementing the rw-lock**: The standard C++ library (since C++14) provides excellent reader-writer lock implementations via [`std::shared_mutex`](https://en.cppreference.com/w/cpp/thread/shared_mutex), [`std::unique_lock`](https://en.cppreference.com/w/cpp/thread/unique_lock), and [`std::shared_lock`](https://en.cppreference.com/w/cpp/thread/shared_lock).
- **Simplify thread-safe access patterns**: We provide a convenient layer that makes it easy to work with shared data in multi-threaded applications without exposing the complexity of manual lock management.
- **Enable safe concurrent access**: Support multiple concurrent readers while ensuring exclusive access for writers, with automatic lock management and RAII semantics.
- **Minimize boilerplate code**: Reduce the amount of locking code needed to safely access shared objects through intuitive APIs.
WE DO NOT IMPLEMENT a read-writer lock; the standard C++ library has one.
We provide a header-only package simplifying the locking code around thread-safe access to your underlying type.
NOT a wrapper; an envelope.
## Why RWLEnvelope?
### The Problem with Manual Lock Management
Writing thread-safe code is hard. Without RWLEnvelope, you'd need to:
```cpp
// Without RWLEnvelope - verbose and error-prone
std::shared_mutex mutex;
std::map data;
// Reading
{
std::shared_lock lock(mutex);
auto it = data.find("key");
if (it != data.end()) {
std::cout << it->second << std::endl;
}
} // Lock released here
// Writing
{
std::unique_lock lock(mutex);
data["key"] = 42;
} // Lock released here
```
### The RWLEnvelope Solution
With RWLEnvelope, the same code becomes cleaner and safer:
```cpp
// With RWLEnvelope - clean and safe
siddiqsoft::RWLEnvelope> data;
// Reading
data.observe([](const auto& m) {
auto it = m.find("key");
if (it != m.end()) {
std::cout << it->second << std::endl;
}
});
// Writing
data.mutate([](auto& m) {
m["key"] = 42;
});
```
### Key Benefits
1. **Automatic Lock Management**: Locks are acquired and released automatically via RAII. No risk of forgetting to unlock.
2. **Clear Intent**: `observe()` for reads and `mutate()` for writes makes your code's intent explicit and self-documenting.
3. **Reduced Boilerplate**: No need to manually create lock objects or manage scopes. The library handles it.
4. **Type Safety**: The template enforces that you're working with the correct type. No accidental type mismatches.
5. **Exception Safe**: If your callback throws, the lock is still released properly. No deadlocks or resource leaks.
6. **Flexible Access Patterns**: Choose between callback-based access (for simple operations) or direct lock access (for complex operations).
7. **Zero Overhead**: Header-only implementation with no runtime overhead beyond the standard library's mutex.
8. **Works with Any Type**: Not limited to JSON or maps. Works with any type that supports move semantics.
### Real-World Scenarios
**Configuration Management**:
```cpp
siddiqsoft::RWLEnvelope config;
// Multiple threads reading config
config.observe([](const auto& cfg) {
return cfg.getDatabaseUrl();
});
// Single thread updating config
config.mutate([](auto& cfg) {
cfg.setDatabaseUrl("new_url");
});
```
**Cache Implementation**:
```cpp
siddiqsoft::RWLEnvelope> cache;
// Fast concurrent reads
cache.observe([](const auto& c) {
return c.at("key");
});
// Exclusive writes
cache.mutate([](auto& c) {
c["key"] = computeValue();
});
```
**Shared State in Services**:
```cpp
siddiqsoft::RWLEnvelope state;
// Multiple reader threads
state.observe([](const auto& s) {
return s.isHealthy();
});
// Single writer thread
state.mutate([](auto& s) {
s.updateMetrics();
});
```
# API Documentation
For comprehensive API documentation, including detailed descriptions of all methods, usage patterns, and examples, see [**API.md**](API.md).
# Requirements
- You must be able to use [``](https://en.cppreference.com/w/cpp/thread/shared_mutex) and [``](https://en.cppreference.com/w/cpp/thread/mutex).
- Minimal target is `C++17`.
- The build and tests are for Visual Studio 2019 under x64.
- We use [`nlohmann::json`](https://github.com/nlohmann/json) only in our tests and the library is aware to provide a conversion operator if library is detected.
# Usage
- Use the nuget [SiddiqSoft.RWLEnvelope](https://www.nuget.org/packages/SiddiqSoft.RWLEnvelope/)
- Copy paste..whatever works.
- The idea is to not "wrap" the underlying type forcing you to either inherit or re-implement the types but to take advantage of the underlying type's interface whilst ensuring that we have the necessary locks.
- Two methods:
- Observer/mutator model with callback and custom return to limit access and to focus the where and how to access the underlying type.
- Take advantage of [init-statement in if-statement](https://en.cppreference.com/w/cpp/language/if) to get the contained object within a lock and have the compiler auto-release once we leave scope.
- A sample implementation (say you want a std::map with reader-writer lock)
- `using RWLMap = siddiqsoft::RWLEnvelope;`
```cpp
#include "gtest/gtest.h"
#include "nlohmann/json.hpp"
#include "siddiqsoft/RWLEnvelope.hpp"
TEST(examples, AssignWithCallbacks)
{
siddiqsoft::RWLEnvelope docl; // we will assign later
nlohmann::json doc2 {{"baa", 0x0baa}, {"fee", 0x0fee}, {"bee", 0x0bee}};
// Move assign here post init
docl.reassign(std::move(doc2));
// Must be empty since we moved it into the envelope
EXPECT_TRUE(doc2.empty());
// Check we have pre-change value.. Note that here we return a boolean to avoid data copy
EXPECT_TRUE(docl.observe([](const auto& doc) -> bool {
return (doc.value("fee", 0xfa17) == 0x0fee) && (doc.value("baa", 0xfa17) == 0x0baa) && (doc.value("bee", 0xfa17) == 0x0bee);
}));
EXPECT_EQ(3, docl.observe([](const auto& doc) { return doc.size(); }));
}
TEST(examples, AssignWithDirectLocks)
{
siddiqsoft::RWLEnvelope docl({{"foo", "bar"}, {"few", "lar"}});
nlohmann::json doc2 {{"baa", 0x0baa}, {"fee", 0x0fee}, {"bee", 0x0bee}};
// Previous document has two items..
if (auto const& [doc, rl] = docl.readLock(); rl) { EXPECT_EQ(2, doc.size()); }
// Modify the item (replace the initial with new)
if (auto [doc, wl] = docl.writeLock(); wl) { doc = std::move(doc2); };
//doc2 -> Must be empty since we moved it into the envelope
EXPECT_TRUE(doc2.empty());
// Check we have post-change value..
if (const auto& [doc, rl] = docl.readLock(); rl) { EXPECT_EQ(3, doc.size()); }
}
```
Additional [examples](tests/examples.cpp).
# Test Coverage
The library includes comprehensive test coverage across multiple categories:
## Basic Functionality Tests
- **Simple Operations**: Basic envelope creation and mutation
- **Callback-Based Access**: Testing `observe()` and `mutate()` methods with various return types
- **Direct Lock Access**: Testing `readLock()` and `writeLock()` with structured bindings
- **Reassignment**: Testing `reassign()` method for replacing envelope contents
- **Snapshot Operations**: Testing `snapshot()` for independent copies
- **Move Semantics**: Testing move constructors and move assignment
## Edge Case Tests
- **Default Construction**: Envelopes with default-constructed objects
- **Return Value Forwarding**: Callbacks returning various types (void, int, string, bool, size_t)
- **Non-JSON Types**: Testing with `std::vector`, `std::string`, and other types
- **Move Constructor Behavior**: Verifying source envelope state after move operations
- **RWA Counter Tracking**: Validating the read-write-action counter accuracy
- **Exception Safety**: Testing behavior when callbacks throw exceptions
- **Independent Snapshots**: Verifying snapshots are truly independent copies
- **Multiple Reassignments**: Testing repeated reassignment operations
## Concurrency & Stress Tests
### Reader-Writer Contention
- **Two-Thread Tests**: Concurrent readers and writers with callbacks and direct locks
- **Monotonic Counter Integrity**: Verifying counter never goes backwards under concurrent access
- **Snapshot Consistency**: Ensuring snapshots return internally consistent state
- **Concurrent Reassign**: Testing reassign racing with observe and snapshot operations
### High-Contention Scenarios
- **Zero-Sleep Maximum Contention**: All threads hammer the lock without delays
- **Mixed API Concurrency**: All 5 API methods used concurrently on the same envelope
- **Shared Read Lock Concurrency**: Multiple readers accessing simultaneously without blocking
- **Concurrent Observe with Return**: Readers returning values under write contention
### Data Integrity Verification
- **RWA Counter Accuracy**: Verifying mutation counter matches exact mutate() count
- **Paired Field Consistency**: Ensuring related fields remain synchronized
- **Version-Data Pairing**: Validating version and data fields stay in sync during reassignment
## Test Statistics
- **Total Test Cases**: 20+ comprehensive test cases
- **Concurrency Levels**: Tests with up to 16 concurrent reader threads and 8 writer threads
- **Iteration Counts**: Stress tests with 5,000-10,000 iterations per thread
- **Coverage Areas**:
- ✅ All public API methods
- ✅ Thread safety guarantees
- ✅ Lock semantics (shared vs. exclusive)
- ✅ Exception safety
- ✅ Move semantics
- ✅ Data consistency under contention
- ✅ Return value forwarding
- ✅ JSON serialization (when available)
## Running Tests
Tests are built using Google Test (gtest) and can be run via the CMake build system:
```bash
cmake --preset Apple-Debug
cmake --build --preset Apple-Debug
ctest --preset Apple-Debug
```
Coverage reports are generated and tracked via Azure Pipelines CI/CD.
For detailed test results and analysis, see [**TEST_RESULTS.md**](TEST_RESULTS.md).
# Test Quality & Reliability
## Comprehensive Test Suite
We take testing seriously. The library includes **38 comprehensive tests** covering:
- ✅ **4 Example tests** - Real-world usage patterns
- ✅ **4 Critical race condition tests** - Writer-writer contention, deadlock prevention, reassign-mutate racing
- ✅ **3 High-priority race condition tests** - Exception safety, API equivalence verification
- ✅ **2 Medium-priority race condition tests** - Snapshot isolation, RWA counter accuracy
- ✅ **3 Low-priority race condition tests** - Memory visibility, variable lock durations
- ✅ **4 Basic concurrency tests** - Multi-threaded reader-writer scenarios
- ✅ **10 Edge case tests** - Exception handling, move semantics, type flexibility
- ✅ **8 Stress tests** - High-contention scenarios with 5,000-10,000 iterations
## Test Execution
All 38 tests pass successfully in ~7.6 seconds:
- **Zero failures** - 100% pass rate
- **No race conditions detected** - Verified with up to 16 concurrent threads
- **Exception safe** - Callbacks throwing exceptions don't corrupt state
- **Deadlock-free** - Explicit timeout-based deadlock detection
- **Memory safe** - All writes visible to readers, no torn reads
## Stress Testing
The test suite includes aggressive stress tests:
- **Maximum contention**: All threads hammer the lock without delays
- **Mixed API usage**: All 5 API methods used concurrently
- **Variable lock durations**: Realistic lock hold times
- **Concurrent exceptions**: Exception safety under contention
- **Rapid reassignments**: Multiple threads reassigning simultaneously
# Feedback & Contributions
## Report Issues or Suggest Improvements
We want to hear about your usage scenarios! If you encounter:
- **Edge cases** not covered by our tests
- **Performance issues** in your specific use case
- **Compatibility problems** with your type or compiler
- **Feature requests** for additional functionality
- **Documentation gaps** or unclear explanations
**Please open an issue** on [GitHub](https://github.com/SiddiqSoft/RWLEnvelope/issues) with:
1. **Your use case**: How are you using RWLEnvelope?
2. **The scenario**: What specific situation triggered the issue?
3. **Expected behavior**: What should happen?
4. **Actual behavior**: What actually happened?
5. **Reproduction steps**: How can we reproduce it?
6. **Environment**: Compiler, OS, C++ version
## Help Us Improve
Your feedback helps us:
- Identify edge cases we haven't tested
- Optimize for real-world usage patterns
- Improve documentation and examples
- Ensure the library works reliably in your scenarios
Even if you don't have issues, we'd love to hear about:
- How you're using RWLEnvelope
- Performance characteristics in your application
- Types you're enveloping
- Patterns that work well for you
## Contributing
Contributions are welcome! Whether it's:
- Additional test cases for your scenarios
- Performance optimizations
- Documentation improvements
- Bug fixes
Please submit a pull request or open an issue to discuss your ideas.
© 2021 Siddiq Software LLC. All rights reserved.