📜 ⬆️ ⬇️

Competitiveness in an asynchronous application on the example of twisted

Theoretically, the problem of competitive access is not typical for asynchronous applications. Unlike applications with a parallel architecture, in which several tasks claiming for some common resource can be performed at a time, in an asynchronous application, only one activity is performed at a time.

But in practice, everything looks a little different:

We present the following situation. Our service works with a remote service (S), which processes requests in parallel. We have resource A, which depends on data on S. Request R1 updates A, for this it requests data from S. R2 comes at this time, it modifies the corresponding data on S. Request R2 comes later, but because of the parallel architecture S - processed earlier (see Fig.1).

image
Fig.1. The order of the query.
')
Now R1 receives data already with modifications R2, while resource A does not yet know that this is the result of R2 operations. Here is the race condition.

Example:

Kolya has a wife and a bank account. Kohl and his wife decided that they have equal rights and a common budget in their family, and therefore each of them has a card tied to this account. Kohl works on freelancing and the time comes when he should pay for the latest project.
His wife decides to please Kohl, and give him a new iPad2 for DR. Getting up early in the morning, she finds N $ in the account! Joyful, she orders her husband’s iPad2 and her new watch. When Kohl checks the bank account, there is "still" zero. He does not guess to check the recent operations and for a long time he swears with the customer.

Kohl - request 1, his wife - request 2, a bank account - a resource on a remote service, bank cards - a proxy for a remote resource.

In place of Kolya, his wife and bank accounts can be: financial services, process control systems and much more. The essence of my problem specifically can not disclose, as the NDA.

How to fight.

We will fight with the use of twisted. Although the overall concept is true for any asynchronous framework.

Obviously, the solution is to lock the resource A. Theoretically, the same constructions will be suitable for blocking as for the parallel architecture: semaphor, mutex, conditional variable. The question of implementation. So, consider mutex (why mutex? Because the conditional variable is trivial, and for semaphor I haven’t managed to find an application yet).

How to make a mutex?

Option non-working. In a parallel architecture, the stream can simply sleep, and we will not get anything for it. In an asynchronous architecture - if we just fall asleep (for example, time.sleep (100)) - then everything will be a stake, and we will never wait, because we need the eventloop to switch to processing the request that blocked the resource, and while we are sleeping - will not happen.

The option is wrong. You can implement it via reactor.callLater (1, self.some_method, * args), where self.some_method is our method that waits for the end of the lock.

The disadvantages are as follows:


Option. And finally, the right option. We have an asynchronous application built on Deferreds. In order for something to happen in it, it is necessary for Deferred to work. Conclusion - the lock must be done on Deferred'ah.

class Mutex(object): def __init__(self): self.locked = False self.waiters = list() def acquire(self): d = Deferred() if self.locked: self.waiters.append(d) else: self.locked = True d.callback(True) return d def release(self): self.locked = False if self.waiters: self.locked = True d = self.waiters.pop() d.callback(True) 

The construction is simple: if we want to access the resource, we get Deferred. We start working with a resource only after Deferred is triggered. The class itself tracks the testing of the Deferreds, and as soon as one of them works out, he gets the next one out of the queue and starts it.

The example implementation is not complete, the twisted (DeferredLock) mutex implementation can be viewed here: http://twistedmatrix.com/trac/browser/tags/releases/twisted-11.0.0/twisted/internet/defer.py . There is also DeferredSemaphore.

An example of use.

You can use it in different ways: you can make the object itself monitor access to it:

 class ImportantObject(object): def __init__(self): self.lock = defer.DeferredLock() def get_lock(self): return self.lock.acquire() def release_lock(self): return self.lock.release() 


Or, if objects are selected from the database and stored in sessions (that is, for each request, the ImportantObject object with id 1 will be different), you can make a pool of locks:

 class Pool(object): __metaclass__ = Singleton def __init__(self, objects_list): self.__objects = dict() for o in objects_list: self.__objects[o.id] = defer.DeferredLock() def acquire(self, o): if o.id not in self.__objects: self.__objects[o.id] = defer.DeferredLock() return self.__objects[o.id].acquire() def release(self, o): self.__objects[o.id].release() 


Pool here is slightly simplified, we “forgot” the methods for updating the list of monitored objects so as not to clutter up the article. We also missed the implementation of Singleton, but finding / making it would not be difficult even for a beginner pythonist.

And finally:

 def multiplex(self, a): def get_value_from_remote_service(skipped, a): d = some_service.do_long_boring_call(a) return d def multiply(result, a): return result*a d = Pool().acquire(a) d.addCallback(get_value_from_remote_service, a) d.addCallback(power,a) return d 


PS: Unfortunately, the twisted documentation is far from exhaustive. Moreover, it covers well if 30 percent of this framework. Therefore, when I solved the problem of competitiveness - I invented various bicycles for 3 days. Until I thought to see the source twisted. So the general advice - working with twisted - read more the source code, there are hidden bicycles for all occasions.

Sources:
Official documentation twisted.
Twisted sources

Source: https://habr.com/ru/post/127812/


All Articles