Thank you for all for all the answers so far, particularly to Ilya and Jean-Paul who provided some very helpful code samples.

It's interesting to realise that, by avoiding locking, we can end up with a much more efficient implementation. I'll have to figure out how widely we can apply this technique - and how often it's going to be worth rewriting things to allow that. Thanks for some useful pointers!


Richard


On 12/03/18 20:00, Jean-Paul Calderone wrote:
On Mon, Mar 12, 2018 at 3:52 PM, Ilya Skriblovsky <ilyaskriblov...@gmail.com <mailto:ilyaskriblov...@gmail.com>> wrote:

    Hi, Richard,

    I've used class like this to cache the result of Expensive
    Calculation:

    class DeferredCache:
    pending = None
    result = None
    failure = None

    def __init__(self, expensive_func):
      self.expensive_func = expensive_func

    def __call__(self):
      if self.pending is None:
          def on_ready(result):
              self.result = result
          def on_fail(failure):
              self.failure = failure

          self.pending =
    defer.maybeDeferred(self.expensive_func).addCallbacks(on_ready,
    on_fail)

      return self.pending.addCallback(self._return_result)


This seems like basically a correct answer to me. However, I suggest one small change.

You probably want to create and return a new Deferred for each result.  If you don't, then your internal `pending` Deferred is now reachable by application code.

As written, an application might (very, very reasonably):

    d = getResource()
    d.addCallback(long_async_operation)

Now `pending` has `long_async_operation` as a callback on its chain.  This will prevent anyone else from getting a result until `long_async_operation` is done.

You can fix this by:

    result = Deferred()
self.pending.addCallback(self._return_result).chainDeferred(result)
    return result

Now the application can only reach `result`.  Nothing they do to `result` will make much difference to `pending` because `chainDeferred` only puts `callback` (and `errback`) onto `pending`'s callback chain.  `callback` and `errback` don't wait on anything.

You have to be a little careful with `chainDeferred` because it doesn't have the recursion-avoidance logic that implicit chaining has.  However, that doesn't matter in this particular case because the chain depth is fixed at two (`pending` and `result`).  The problems only arise if you extend the chain out in this direction without bound.

Jean-Paul

    def _return_result(self, _):
      return self.failure or self.result

    Using it you can get rid of DeferredLocks:

        deferred_cache = DeferredCache(do_expensive_calculation)

        def getResource():
            return deferred_cache()

    It will start `expensive_func` on the first call. The second and
    consequtive calls will return deferreds that resolves with the
    result when expensive_func is done. If you call it when result is
    already here, it will return alread-fired deferred.

    Of course, it will require some more work if you need to pass
    arguments to `expensive_func` and memoize results per arguments
    values.

    -- ilya


_______________________________________________
Twisted-Python mailing list
Twisted-Python@twistedmatrix.com
https://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-python

Reply via email to