"""
Prevents inconsistent db updates for flow.
"""
import time
import random
from contextlib import contextmanager

from django.core.cache import cache as default_cache
from django.db import transaction, DatabaseError

from viewflow.exceptions import FlowLockFailed


def no_lock(flow):
    """
    No pessimistic locking, just execute flow task in transaction.
    Not suitable when you have Join nodes in your flow.
    """
    @contextmanager
    def lock(flow_class, process_pk):
        with transaction.atomic():
            yield
    return lock


def select_for_update_lock(flow, nowait=True, attempts=5):
    """
    Uses `select ... for update` on process instance row for locking,
    bound to database transaction.

    Recommended for use with PostgreSQL.
    """
    @contextmanager
    def lock(flow_class, process_pk):
        with transaction.atomic():
            for i in range(attempts):
                try:
                    process = flow_class.process_class._default_manager.filter(pk=process_pk)
                    if not process.select_for_update(nowait=nowait).exists():
                        raise DatabaseError('Process not exists')
                    break
                except DatabaseError:
                    if i != attempts - 1:
                        sleep_time = (((i + 1) * random.random()) + 2 ** i) / 2.5
                        time.sleep(sleep_time)
                    else:
                        raise FlowLockFailed('Lock failed for {}'.format(flow_class))

            yield

    return lock


class CacheLock(object):
    """
    Task lock based on Django's cache.

    Example::

        class MyFlow(Flow):
            lock_impl = RedisLock(cache=caches['locks'])

    The example uses a different cache. The default cache
    is Django's ``default`` cache configuration.
    """

    def __init__(self, cache=default_cache):
        self.cache = cache

    def __call__(self, flow, attempts=5, expires=120):
        cache = self.cache

        @contextmanager
        def lock(flow_class, process_pk):
            key = 'django-viewflow-lock-{}/{}'.format(flow_class._meta.flow_label, process_pk)

            for i in range(attempts):
                process = flow_class.process_class._default_manager.filter(pk=process_pk)
                if process.exists():
                    stored = cache.add(key, 1, expires)
                    if stored:
                        break
                if i != attempts - 1:
                    sleep_time = (((i + 1) * random.random()) + 2 ** i) / 2.5
                    time.sleep(sleep_time)
            else:
                raise FlowLockFailed('Lock failed for {}'.format(flow_class))

            try:
                with transaction.atomic():
                    yield
            finally:
                cache.delete(key)

        return lock


cache_lock = CacheLock()
"""
Use it if primary cache backend has transactional `add` functionality,
like `memcached`.
"""

@@ -802,34 +802,32 @@
 def lock(flow_cl
-as
 s, process_pk):%0A
@@ -838,39 +838,21 @@
     
-with transaction.atomic():%0A    
+done = False%0A
     
@@ -885,39 +885,121 @@
 s):%0A            
-    try
+if done:%0A                break%0A            try:%0A                with transaction.atomic()
 :%0A              
@@ -1013,34 +1013,32 @@
 rocess = flow_cl
-as
 s.process_class.
@@ -1025,34 +1025,32 @@
 w_cls.process_cl
-as
 s._default_manag
@@ -1205,16 +1205,21 @@
 Process 
+does 
 not exis
@@ -1219,17 +1219,17 @@
 ot exist
-s
+.
 ')%0A     
@@ -1239,34 +1239,62 @@
             
-break
+done = True
 %0A    
+                yield%0A
             
@@ -1315,20 +1315,16 @@
 eError:%0A
-    
         
@@ -1345,36 +1345,32 @@
 = attempts - 1:%0A
-    
                 
@@ -1443,36 +1443,32 @@
                 
-    
 time.sleep(sleep
@@ -1466,36 +1466,32 @@
 eep(sleep_time)%0A
-    
                 
@@ -1508,36 +1508,32 @@
                 
-    
 raise FlowLockFa
@@ -1576,34 +1576,13 @@
 w_cl
-as
 s))%0A%0A
-            yield%0A%0A
     

Refactor select_for_update_lock