Search code examples
c#memoryidisposable

Transaction scope similar functionality


I am looking to setup something very similar to transaction scope which creates a version on a service and will delete/commit at the end of scope. Every SQL statement ran inside the transaction scope internally looks at some connection pool / transaction storage to determine if its in the scope and reacts appropriately. The caller doesn't need to pass in the transaction to every call. I am looking for this functionality.

Here is a little more about it: https://blogs.msdn.microsoft.com/florinlazar/2005/04/19/transaction-current-and-ambient-transactions/

Here is the basic disposable class:

public sealed class VersionScope : IDisposable
{
    private readonly GeodatabaseVersion _version;
    private readonly VersionManager _versionManager;

    public VersionScope(Configuration config)
    {
        _versionManager = new VersionManager(config);
        _version = _versionManager.GenerateTempVersion();
        _versionManager.Create(_version);
        _versionManager.VerifyValidVersion(_version);
        _versionManager.ServiceReconcilePull();
        _versionManager.ReconcilePull(_version);
    }

    public void Dispose()
    {
        _versionManager.Delete(_version);
    }

    public void Complete()
    {
        _versionManager.ReconcilePush(_version);
    }
}

I want the ability for all the code I've written thus far to not have any concept of being in a version. I just want to include a simple

Version = GetCurrentVersionWithinScope()

at the lowest level of the code.

What is the safest way of implementing something like this with little risk of using the wrong version if there are multiple instances in memory simultaneously running.

My very naive approach would be find if there is a unique identifier for a block of memory a process is running in. Then store the current working version to a global array or concurrent dictionary. Then in the code where I need the current version, I use its block of memory identifier and it maps to the version that was created.

Edit:

Example of usage:

using (var scope = new VersionScope(_config))
{
    AddFeature(); // This has no concept of scope passed to it, and could error out forcing a dispose() without a complete()
    scope.Complete();
}

Solution

  • The most straightforward approach would be to use ThreadStatic or ThreadLocal to store current version in thread local storage. That way multiple threads will not interfere with each other. For example suppose we version class:

    public class Version {
        public Version(int number) {
            Number = number;
        }
        public int Number { get; }
    
        public override string ToString() {
            return "Version " + Number;
        }
    }
    

    Then implementation of VersionScope can go like this:

    public sealed class VersionScope : IDisposable {
        private bool _isCompleted;
        private bool _isDisposed;
        // note ThreadStatic attribute
        [ThreadStatic] private static Version _currentVersion;
        public static Version CurrentVersion => _currentVersion;
    
        public VersionScope(int version) {
            _currentVersion = new Version(version);
        }
    
        public void Dispose() {
            if (_isCompleted || _isDisposed)
                return;
            var v = _currentVersion;
            if (v != null) {
                DeleteVersion(v);
            }
            _currentVersion = null;
            _isDisposed = true;
        }
    
        public void Complete() {
            if (_isCompleted || _isDisposed)
                return;
            var v = _currentVersion;
            if (v != null) {
                PushVersion(v);
            }
            _currentVersion = null;
            _isCompleted = true;
        }
    
        private void DeleteVersion(Version version) {
            Console.WriteLine($"Version {version} deleted");
        }
    
        private void PushVersion(Version version) {
            Console.WriteLine($"Version {version} pushed");
        }
    }
    

    It will work, but it will not support nested scopes, which is not good, so to fix we need to store previous scope when starting new one, and restore it on Complete or Dispose:

    public sealed class VersionScope : IDisposable {
        private bool _isCompleted;
        private bool _isDisposed;
        private static readonly ThreadLocal<VersionChain> _versions = new ThreadLocal<VersionChain>();
    
        public static Version CurrentVersion => _versions.Value?.Current;
    
        public VersionScope(int version) {
            var cur = _versions.Value;
            // remember previous versions if any
            _versions.Value = new VersionChain(new Version(version), cur);
        }
    
        public void Dispose() {
            if (_isCompleted || _isDisposed)
                return;
            var cur = _versions.Value;
            if (cur != null) {
                DeleteVersion(cur.Current);
                // restore previous
                _versions.Value = cur.Previous;
            }
            _isDisposed = true;
        }
    
        public void Complete() {
            if (_isCompleted || _isDisposed)
                return;
            var cur = _versions.Value;
            if (cur != null) {
                PushVersion(cur.Current);
                // restore previous
                _versions.Value = cur.Previous;
            }
            _isCompleted = true;
        }
    
        private void DeleteVersion(Version version) {
            Console.WriteLine($"Version {version} deleted");
        }
    
        private void PushVersion(Version version) {
            Console.WriteLine($"Version {version} pushed");
        }
    
        // just a class to store previous versions
        private class VersionChain {
            public VersionChain(Version current, VersionChain previous) {
                Current = current;
                Previous = previous;
            }
    
            public Version Current { get; }
            public VersionChain Previous { get; }
        }
    }
    

    That's already something you can work with. Sample usage (I use single thread, but if there were multiple threads doing this separately - they will not interfere with each other):

    static void Main(string[] args) {
        PrintCurrentVersion(); // no version
        using (var s1 = new VersionScope(1)) {
            PrintCurrentVersion(); // version 1
            s1.Complete();
            PrintCurrentVersion(); // no version, 1 is already completed
            using (var s2 = new VersionScope(2)) {
                using (var s3 = new VersionScope(3)) {
                    PrintCurrentVersion(); // version 3
                } // version 3 deleted
                PrintCurrentVersion(); // back to version 2
                s2.Complete();
            }
            PrintCurrentVersion(); // no version, all completed or deleted
        }
        Console.ReadKey();
    }
    
    private static void PrintCurrentVersion() {
        Console.WriteLine("Current version: " + VersionScope.CurrentVersion);
    }
    

    This however will not work when you are using async calls, because ThreadLocal is tied to a thread, but async method can span multiple threads. However, there is similar construct named AsyncLocal, which value will flow through asynchronous calls. So we can add constructor parameter to VersionScope indicating if we need async flow or not. Transaction scope works in a similar way - there is TransactionScopeAsyncFlowOption you pass into TransactionScope constructor indicating if it will flow through async calls.

    Modified version looks like this:

    public sealed class VersionScope : IDisposable {
        private bool _isCompleted;
        private bool _isDisposed;
        private readonly bool _asyncFlow;
        // thread local versions
        private static readonly ThreadLocal<VersionChain> _tlVersions = new ThreadLocal<VersionChain>();
        // async local versions
        private static readonly AsyncLocal<VersionChain> _alVersions = new AsyncLocal<VersionChain>();
        // to get current version, first check async local storage, then thread local
        public static Version CurrentVersion => _alVersions.Value?.Current ?? _tlVersions.Value?.Current;
        // helper method
        private VersionChain CurrentVersionChain => _asyncFlow ? _alVersions.Value : _tlVersions.Value;
    
        public VersionScope(int version, bool asyncFlow = false) {
            _asyncFlow = asyncFlow;
    
            var cur = CurrentVersionChain;
            // remember previous versions if any
            if (asyncFlow) {
                _alVersions.Value = new VersionChain(new Version(version), cur);
            }
            else {
                _tlVersions.Value = new VersionChain(new Version(version), cur);
            }
        }
    
        public void Dispose() {
            if (_isCompleted || _isDisposed)
                return;
            var cur = CurrentVersionChain;
            if (cur != null) {
                DeleteVersion(cur.Current);
                // restore previous
                if (_asyncFlow) {
                    _alVersions.Value = cur.Previous;
                }
                else {
                    _tlVersions.Value = cur.Previous;
                }
            }
            _isDisposed = true;
        }
    
        public void Complete() {
            if (_isCompleted || _isDisposed)
                return;
            var cur = CurrentVersionChain;
            if (cur != null) {
                PushVersion(cur.Current);
                // restore previous
                if (_asyncFlow) {
                    _alVersions.Value = cur.Previous;
                }
                else {
                    _tlVersions.Value = cur.Previous;
                }
            }
            _isCompleted = true;
        }
    
        private void DeleteVersion(Version version) {
            Console.WriteLine($"Version {version} deleted");
        }
    
        private void PushVersion(Version version) {
            Console.WriteLine($"Version {version} pushed");
        }
    
        // just a class to store previous versions
        private class VersionChain {
            public VersionChain(Version current, VersionChain previous) {
                Current = current;
                Previous = previous;
            }
    
            public Version Current { get; }
            public VersionChain Previous { get; }
        }
    }
    

    Sample usage of scopes with async flow:

    static void Main(string[] args) {
        Test();
        Console.ReadKey();
    }
    
    static async void Test() {
        PrintCurrentVersion(); // no version
        using (var s1 = new VersionScope(1, asyncFlow: true)) {
            await Task.Delay(100);
            PrintCurrentVersion(); // version 1
            await Task.Delay(100);
            s1.Complete();
            await Task.Delay(100);
            PrintCurrentVersion(); // no version, 1 is already completed
            using (var s2 = new VersionScope(2, asyncFlow: true)) {
                using (var s3 = new VersionScope(3, asyncFlow: true)) {
                    PrintCurrentVersion(); // version 3
                } // version 3 deleted
                await Task.Delay(100);
                PrintCurrentVersion(); // back to version 2
                s2.Complete();
            }
            await Task.Delay(100);
            PrintCurrentVersion(); // no version, all completed or deleted
        }
    }
    
    private static void PrintCurrentVersion() {
        Console.WriteLine("Current version: " + VersionScope.CurrentVersion);
    }