SiddiqSoft.RWLEnvelope 1.4.3

dotnet add package SiddiqSoft.RWLEnvelope --version 1.4.3
                    
NuGet\Install-Package SiddiqSoft.RWLEnvelope -Version 1.4.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SiddiqSoft.RWLEnvelope" Version="1.4.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SiddiqSoft.RWLEnvelope" Version="1.4.3" />
                    
Directory.Packages.props
<PackageReference Include="SiddiqSoft.RWLEnvelope" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SiddiqSoft.RWLEnvelope --version 1.4.3
                    
#r "nuget: SiddiqSoft.RWLEnvelope, 1.4.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SiddiqSoft.RWLEnvelope@1.4.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SiddiqSoft.RWLEnvelope&version=1.4.3
                    
Install as a Cake Addin
#tool nuget:?package=SiddiqSoft.RWLEnvelope&version=1.4.3
                    
Install as a Cake Tool

RWLEnvelope : A simple read-writer lock envelope

CodeQL Build Status alternate text is missing from this package README image alternate text is missing from this package README image alternate text is missing from this package README image alternate text is missing from this package README image

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, std::unique_lock, and std::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.

<p align="right" width="50%"> <b>WE DO NOT IMPLEMENT</b> a read-writer lock; the standard C++ library has one.<br/>We provide a header-only package simplifying the locking code around thread-safe access to your underlying type. <br/> <i>NOT a wrapper; an envelope.</i> </p>

Why RWLEnvelope?

The Problem with Manual Lock Management

Writing thread-safe code is hard. Without RWLEnvelope, you'd need to:

// Without RWLEnvelope - verbose and error-prone
std::shared_mutex mutex;
std::map<std::string, int> 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:

// With RWLEnvelope - clean and safe
siddiqsoft::RWLEnvelope<std::map<std::string, int>> data;

// Reading
data.observe<void>([](const auto& m) {
    auto it = m.find("key");
    if (it != m.end()) {
        std::cout << it->second << std::endl;
    }
});

// Writing
data.mutate<void>([](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:

siddiqsoft::RWLEnvelope<AppConfig> config;

// Multiple threads reading config
config.observe<std::string>([](const auto& cfg) {
    return cfg.getDatabaseUrl();
});

// Single thread updating config
config.mutate<void>([](auto& cfg) {
    cfg.setDatabaseUrl("new_url");
});

Cache Implementation:

siddiqsoft::RWLEnvelope<std::unordered_map<std::string, CacheEntry>> cache;

// Fast concurrent reads
cache.observe<CacheEntry>([](const auto& c) {
    return c.at("key");
});

// Exclusive writes
cache.mutate<void>([](auto& c) {
    c["key"] = computeValue();
});

Shared State in Services:

siddiqsoft::RWLEnvelope<ServiceState> state;

// Multiple reader threads
state.observe<bool>([](const auto& s) {
    return s.isHealthy();
});

// Single writer thread
state.mutate<void>([](auto& s) {
    s.updateMetrics();
});

API Documentation

For comprehensive API documentation, including detailed descriptions of all methods, usage patterns, and examples, see API.md.

Requirements

  • You must be able to use <shared_mutex> and <mutex>.
  • Minimal target is C++17.
  • The build and tests are for Visual Studio 2019 under x64.
  • We use 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
  • 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 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<std::map>;
#include "gtest/gtest.h"
#include "nlohmann/json.hpp"
#include "siddiqsoft/RWLEnvelope.hpp"


TEST(examples, AssignWithCallbacks)
{
	siddiqsoft::RWLEnvelope<nlohmann::json> 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<bool>([](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<size_t>([](const auto& doc) { return doc.size(); }));
}


TEST(examples, AssignWithDirectLocks)
{
	siddiqsoft::RWLEnvelope<nlohmann::json> 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.

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<int>, 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:

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 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 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.

<small align="right">

© 2021 Siddiq Software LLC. All rights reserved.

</small>

Product Compatible and additional computed target framework versions.
native native is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.3 113 4/29/2026
1.2.0 521 12/7/2024
1.1.1 874 12/2/2021
1.1.0 9,663 7/22/2021
1.1.0-main0001 663 7/22/2021
1.0.0 1,893 7/22/2021
0.10.0-main0001 657 7/22/2021

## Features
- Header-only C++ template library (no compilation needed)
- Simple, intuitive API for thread-safe access patterns
- Automatic lock management with RAII semantics
- Support for both callback-based and direct lock access
- Exception-safe with proper lock release on errors
- Works with any type supporting move semantics
- Zero runtime overhead beyond standard library mutex
- Comprehensive test coverage (38 tests, 100% pass rate)
- Verified deadlock-free and race-condition-free
- Supports C++17 and later

## What's New
- Comprehensive API documentation (API.md)
- 12 new race condition tests for critical scenarios
- Test quality and reliability section in README
- Real-world usage examples and patterns
- Community feedback invitation for edge cases