@Override
    @Nullable
    public V invoke(K input) {
      Object value = cache.get(input);
      if (value != null && value != NotValue.COMPUTING)
        return WrappedValues.unescapeExceptionOrNull(value);

      lock.lock();
      try {
        value = cache.get(input);
        assert value != NotValue.COMPUTING
            : "Recursion detected on input: " + input + " under " + LockBasedStorageManager.this;
        if (value != null) return WrappedValues.unescapeExceptionOrNull(value);

        AssertionError error = null;
        try {
          cache.put(input, NotValue.COMPUTING);
          V typedValue = compute.invoke(input);
          Object oldValue = cache.put(input, WrappedValues.escapeNull(typedValue));

          // This code effectively asserts that oldValue is null
          // The trickery is here because below we catch all exceptions thrown here, and this is the
          // only exception that shouldn't be stored
          // A seemingly obvious way to come about this case would be to declare a special exception
          // class, but the problem is that
          // one memoized function is likely to (indirectly) call another, and if this second one
          // throws this exception, we are screwed
          if (oldValue != NotValue.COMPUTING) {
            error =
                new AssertionError(
                    "Race condition detected on input "
                        + input
                        + ". Old value is "
                        + oldValue
                        + " under "
                        + LockBasedStorageManager.this);
            throw error;
          }

          return typedValue;
        } catch (Throwable throwable) {
          if (throwable == error) throw exceptionHandlingStrategy.handleException(throwable);

          Object oldValue = cache.put(input, WrappedValues.escapeThrowable(throwable));
          assert oldValue == NotValue.COMPUTING
              : "Race condition detected on input "
                  + input
                  + ". Old value is "
                  + oldValue
                  + " under "
                  + LockBasedStorageManager.this;

          throw exceptionHandlingStrategy.handleException(throwable);
        }
      } finally {
        lock.unlock();
      }
    }
    @Override
    public T invoke() {
      Object _value = value;
      if (!(_value instanceof NotValue)) return WrappedValues.unescapeThrowable(_value);

      lock.lock();
      try {
        _value = value;
        if (!(_value instanceof NotValue)) return WrappedValues.unescapeThrowable(_value);

        if (_value == NotValue.COMPUTING) {
          value = NotValue.RECURSION_WAS_DETECTED;
          RecursionDetectedResult<T> result = recursionDetected(/*firstTime = */ true);
          if (!result.isFallThrough()) {
            return result.getValue();
          }
        }

        if (_value == NotValue.RECURSION_WAS_DETECTED) {
          RecursionDetectedResult<T> result = recursionDetected(/*firstTime = */ false);
          if (!result.isFallThrough()) {
            return result.getValue();
          }
        }

        value = NotValue.COMPUTING;
        try {
          T typedValue = computable.invoke();
          value = typedValue;
          postCompute(typedValue);
          return typedValue;
        } catch (Throwable throwable) {
          if (value == NotValue.COMPUTING) {
            // Store only if it's a genuine result, not something thrown through recursionDetected()
            value = WrappedValues.escapeThrowable(throwable);
          }
          throw exceptionHandlingStrategy.handleException(throwable);
        }
      } finally {
        lock.unlock();
      }
    }