How to do a `getOrElseComplete` on `Promise`?

Viewed 81

Does it make sense to have an operation like getOrElseComplete that tries to complete a Promise with a value but, if the Promise is already completed, returns the existing completed value instead. Here's a sample implementation:

implicit class PromiseOps[T](promise: Promise[T]) {
  def getOrElseComplete(value: Try[T]): Try[T] = {
    val didComplete = promise.tryComplete(value)
    if (didComplete) {
      value
    } else {
      // The `tryComplete` returned `false`, which means the promise
      // was already completed, so the following is safe (?)
      promise.future.value.get
    }
  }
}

Is this safe to do? If not, why not? If so, is there a way to do this directly (eg. without relying on questionable things like _.value.get) or why isn't there such a way in the standard library?

2 Answers

From your comments it seems to me that this is a valid solution for your problem but I also feel that a method like this doesn't belong in Promise API because Promise is supposed to be only a settable "backend" of its Future.

I'd prefer to have a completely separate function which could look like this:

def getOrComplete[T](promise: Promise[T], value: Try[T]): Try[T] =
  promise.future.value.getOrElse {
    if (promise.tryComplete(value)) value
    else getOrComplete(promise, value)
  }

The recursive call may seem weird - it serves two purposes:

  • it protects against a race condition where some other thread completes the future just before we call tryComplete
  • it avoids usage of .value.get on the Future

You might also want to pass value as a by-name parameter to avoid evaluating it when the Promise is already completed.

This operation does what it promises. It may make more sense to take value by name, and don't try to complete if already completed, maybe something like

def getOrElseComplete(value: => Try[T]): Try[T] = {
  if (!promise.completed) {
    promise.tryComplete(value)
  }
  promise.future.value.get
}

It's kinda dodgy though. Sharing a promise and having multiple places where it might be completed sounds like a difficult to maintain design, and one has to ask what's happening with the other path that might still complete the Promise? Shouldn't something be cancelled there?

Related