6

According to the ASP.Net Core docs, the behaviour of the session state has changed in that it is now non-locking:

Session state is non-locking. If two requests simultaneously attempt to modify the contents of a session, the last request overrides the first. Session is implemented as a coherent session, which means that all the contents are stored together. When two requests seek to modify different session values, the last request may override session changes made by the first.

My understanding is that this is different to the behaviour of the session in the .Net Framework, where the user's session was locked per request so that whenever you read from/wrote to it, you weren't overwriting another request's data or reading stale data, for that user.

My question(s):

  1. Is there a way to re-enable this per-request locking of the user's session in .Net Core?

  2. If not, is there a reliable way to use the session to prevent duplicate submission of data for a given user? To give a specific example, we have a payment process that involves the user returning from an externally hosted ThreeDSecure (3DS) iFrame (payment card security process). We are noticing that sometimes (somehow) the user is submitting the form within the iFrame multiple times, which we have no control over. As a result this triggers multiple callbacks to our application. In our previous .Net Framework app, we used the session to indicate if a payment was in progress. If this flag was set in the session and you hit the 3DS callback again, the app would stop you proceeding. However, now it seems that because the session isn't locked, when these near simultaneous, duplicate callbacks occur, thread 'A' sets 'payment in progress = true' but thread 'B' doesn't see that in time, it's snapshot of the session is still seeing 'payment in progress = false' and the callback logic is processed twice.

What are some good approaches to handling simultaneous requests accessing the same session, now that the way the session works has changed?

harman_kardon
  • 1,567
  • 5
  • 32
  • 49
  • 11
    Don't use session for storing things so important as a payment status, use your database that is persistent and easily accessible. Session may be lost in multiple scenarios and that's something that you don't want when money is involved... Also, using a unique index you can avoid those doule transactions, I am sure that you can create a unique key for each payment (user id + transaction id, user id + product id, or whatever) so you can use that key with a unique index to avoid reissuing the same payment. – Gusman Jun 24 '20 at 20:21
  • it allows multiple reads and writes... just last write is the winner, what this means... is before it would lock the next read while reading... even if it wasn't going to change. based on this... and that only 1 thread should be updating the value... what stops you from reading it until you have the value you want. aka would need to know more about your processes. as to what is calling back and why... like how would it normally work.... before the change... did it just wait. if so why not let the call just loop until the session value is what your looking for.. not recommending – Seabizkit Jun 26 '20 at 18:44
  • `We are noticing that sometimes (somehow) the user is submitting the form within the iFrame multiple times` prevent them from doing this? – Seabizkit Jun 26 '20 at 18:45
  • This is when coming back from an iframe during the 3DS process - we have no access/control as to how the form in the iframe is submitted. – harman_kardon Jun 26 '20 at 18:46
  • `we used the session to indicate if a payment was in progress` you can still do this.. check the value.. – Seabizkit Jun 26 '20 at 18:46
  • what does it do in the callback.,.. which is related to session...., say the callback get x from session then what... and if it get y then what. – Seabizkit Jun 26 '20 at 18:47
  • does the callback itself indicate that its done? or are you saying it can hit ur callback many times with many different statuses.... if so seem odd.... oh wait i think i get what u saying the user clicks twice.....fast..;-) mmmm odd that the third part system doesn't block this as they can not process the same transaction twice surely.... what stops you from just setting it twice would it be the same value. return from callback thread A, sets to "cat" return from callback thread b set to "cat" done twice but sill same value. – Seabizkit Jun 26 '20 at 19:07
  • it may be easier to understand if you show what you can... regarding the code – Seabizkit Jun 26 '20 at 19:08
  • Just stop using session. always make stateless applications. If you want to lock a resource, do it manually on the exact line it really needs to be locked.. I know this is not an answer to your question, but still, do not use sessions... – Noldor Jun 28 '20 at 04:12

2 Answers2

6

The problem that you have faced with is called Race Condition (stackoverflow, wiki). To cut-through, you'd like to get exclusive access to the session state, you can achieve that in several ways and they highly depend on your architecture.

In-process synchronization

If you have a single machine with a single process handling all requests (for example you use a self-hosted server, Kestrel), you may use lock. Just do it correctly and not how @TMG suggested.

Here is an implementation reference:

  1. Use single global object to lock all threads:
  private static object s_locker = new object();

  public bool Process(string transaction) {
      lock (s_locker) {
        if(!HttpContext.Session.TryGetValue("TransactionId", out _)) {
           ... handle transaction
        }
      }
  }

Pros: a simple solution Cons: all requests from all users will wait on this lock

  1. use per-session lock object. Idea is similar, but instead of a single object you just use a dictionary:
    internal class LockTracker : IDisposable
    {
        private static Dictionary<string, LockTracker> _locks = new Dictionary<string, LockTracker>();
        private int _activeUses = 0;
        private readonly string _id;

        private LockTracker(string id) => _id = id;

        public static LockTracker Get(string id)
        {
            lock(_locks)
            {
                if(!_locks.ContainsKey(id))
                    _locks.Add(id, new LockTracker(id));
                var res = _locks[id];
                res._activeUses += 1;
                return res;
            }
        }

        void IDisposable.Dispose()
        {
            lock(_locks)
            {
                _activeUses--;
                if(_activeUses == 0)
                    _locks.Remove(_id);
            }
        }
    }


public bool Process(string transaction)
{
    var session = HttpContext.Session;
    var locker = LockTracker.Get(session.Id);
    using(locker) // remove object after execution if no other sessions use it
    lock (locker) // synchronize threads on session specific object
    {
        // check if current session has already transaction in progress
        var transactionInProgress = session.TryGetValue("TransactionId", out _);
        if (!transactionInProgress)
        {
            // if there is no transaction, set and handle it
            HttpContext.Session.Set("TransactionId", System.Text.Encoding.UTF8.GetBytes(transaction));
            HttpContext.Session.Set("StartTransaction", BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
            // handle transaction here
        }
        // return whatever you need, here is just a boolean.
        return transactionInProgress;
    }
}

Pros: manages concurrency on the session level Cons: more complex solution

Remember that lock-based option will work only when the same process on the webserver handling all user's requests - lock is intra-process synchronization mechanism! Depending on what you use as a persistent layer for sessions (like NCache or Redis), this option might be the most performant though.

Cross-process synchronization

If there are several processes on the machine (for example you have IIS and apppool is configured to run multiple worker processes), then you need to use kernel-level synchronization primitive, like Mutex.

Cross-machine synchronization

If you have a load balancer (LB) in front of your webfarm so that any of N machines can handle user's request, then getting exclusive access is not so trivial.

One option here is to simplify the problem by enabling the 'sticky session' option in your LB so that all requests from the same user (session) will be routed to the same machine. In this case, you are fine to use any cross-process or in-process synchronization option (depends on what you have running there).

Another option is to externalize synchronization, for example, move it to the transactional DB, something similar to what @HoomanBahreini suggested. Beware that you need to be very cautious on handling failure scenarios: you may mark your session as in progress and then your webserver which handled it crashed leaving it locked in DB.

Important

In all of these options you must ensure that you obtain lock before reading the state and hold it until you update the state.

Please clarify what option is the closest to your case and I can provide more technical details.

fenixil
  • 2,106
  • 7
  • 13
  • Thanks for the suggestions. We're using sticky sessions (not ideal) at the moment. The approach I am working on at the moment is to use the DB to record whether a payment is in progress, utilising locks at the DB level I can prevent mutiple threads for the same session from setting 'payment in progress' to true at the same time... – harman_kardon Jun 29 '20 at 10:07
  • Would you be able to elaborate on this? 'you may use lock. Just do it correctly and not how @TMG suggested.' Would be interested to hear any ideas on how to lock per user's session (in code) – harman_kardon Jun 29 '20 at 10:09
  • 1
    Your second solution is not thread safe, since a thread could remove the entry from the dictionary after another thread already retrieved the object and is waiting for the lock. A third thread would then receive a different lock object and two threads could get into the locked part. Right? – dr0n3 Mar 31 '21 at 06:13
  • @dr0n3 you are totally right, changed the solution to eliminate race. – fenixil Apr 16 '21 at 02:13
2

Session is designed to store temporary user data among multiple requests, a good example is login-state... without session you would have to login to stackoverflow.com every time you open a new question... but the website remembers you, because your send them your session state inside a cookie. According to Microsoft:

The session data is backed by a cache and considered ephemeral data. The site should continue to function without the session data. Critical application data should be stored in the user database and cached in session only as a performance optimization.

It is quite simple to implement a locking mechanism to solve your mutex issue, however the session itself is not a reliable storage and you may loose its content at any time.

How to identify duplicate payments?

The problem is you are getting multiple payment requests and you want to discard the duplicate ones... what's your definition of a duplicate payment?

Your current solution discard the second payment while a first one is in progress... let's say your payment takes 2 seconds to complete... what will happen if you receive the duplicate payment after 3 seconds?

Every reliable payment system includes a unique PaymentId in their request... what you need to do is to mark this PaymentId as processed in your DB. This way you won't process the same payment twice, no matter when the duplicate request arrives.

You can use a Unique Constraint on PaymentId to prevent duplicate payments:

public bool ProcessPayment(Payment payment) {
    bool res = InsertIntoDb(payment);
    if (res == false) {
        return false; // <-- insert has failed because PaymentId is not unique
    }        
    
    Process(payment);
    return true;
}

Same example using lock:

public class SafePayment {
    private static readonly Object lockObject = new Object();

    public bool ProcessPayment(Payment payment) {
        lock (lockObject) {
            var duplicatePayment = ReadFromDb(payment.Id);
            if (duplicatePayment != null) {
                return false; // <-- duplicate 
            }
            
            Process(payment);
            WriteToDb(payment);
            return true;
        }
    }
}
Hooman Bahreini
  • 14,480
  • 11
  • 70
  • 137
  • your example with DB does not eliminate race condition at all: 2 thread can read state from DB, see that PaymentInProgress is false and continue processing: 1st thread will hold lock and execute logic, 2nd thread will wait for log and !execute logic! . You should either use double-check locking (https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_C%23) or eliminate race in DB, by using concurrency tokens for example (https://learn.microsoft.com/en-us/ef/core/modeling/concurrency?tabs=data-annotations). – fenixil Jun 29 '20 at 02:33
  • Locking the same object for every request will only allow one payment to be processed at a time, obviously not acceptable for a busy ecommerce site! – harman_kardon Jun 29 '20 at 10:05
  • My question specifically mentions 'per-request locking of the user's session' - this is an example of locking, but your example would not lock just the session for the request to which it belongs, it would lock access to the session for ALL requests and force them to execute one a time. – harman_kardon Jun 29 '20 at 16:16