Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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();
}
like image 908
CuriousDeveloper Avatar asked Feb 23 '18 16:02

CuriousDeveloper


People also ask

What is a transaction scope?

The TransactionScope class provides a simple way to mark a block of code as participating in a transaction, without requiring you to interact with the transaction itself. A transaction scope can select and manage the ambient transaction automatically.

What is transaction scope in Entity Framework?

Entity Framework is already operating within a TransactionScope. The connection object in the transaction passed is null. That is, the transaction is not associated with a connection – usually this is a sign that that transaction has already completed.

What is transaction scope in SQL Server?

Definition: TransactionalScope makes your code block Transactional. You can easily maintain one transaction for multiple databases or a single database with multiple connectionstrings, using TransactionScope. When you use TransactionScope there is no need to close any Database connections in the middle.

What is an ambient transaction?

An ambient transaction is one that works at the thread level. Thus, all operations that occur in that context will be part of the transaction.


1 Answers

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);
}
like image 123
Evk Avatar answered Oct 08 '22 03:10

Evk