Skip to content

AsyncCallbacks lost on finished stage by error#23185

Merged
patriknw merged 2 commits into
akka:masterfrom
agolubev:23111-AsyncCallbacks_lost_finished_stage-agolubev
Nov 9, 2017
Merged

AsyncCallbacks lost on finished stage by error#23185
patriknw merged 2 commits into
akka:masterfrom
agolubev:23111-AsyncCallbacks_lost_finished_stage-agolubev

Conversation

@agolubev

Copy link
Copy Markdown
Contributor

Ref #23111
Please review approach of adding all incoming requests to queue with acknowledge future.
In case the stream is finished but some futures stay not completed - we are failing them automatically.
There is a plan to substitute CallBackWapper with ConcurrentGraphStage everywhere (perhaps need a new name to explain that stage allows making callbacks not taking into account stream lifecycle.)

Let me know what do you think

There still some sync points in new GraphStage this is because we need to synchronize ideal Akka world with the not ideal external world. Initially, it was considered to introduce special actor with extended lifecycle to avoid sync points - but this was rejected.
Any ideas as to avoid synchronization are welcome.

@akka-ci akka-ci added validating PR is currently being validated by Jenkins needs-attention Indicates a PR validation failure (set by CI infrastructure) and removed validating PR is currently being validated by Jenkins labels Jun 17, 2017
@akka-ci

akka-ci commented Jun 17, 2017

Copy link
Copy Markdown

Test FAILed.

@akka-ci akka-ci added validating PR is currently being validated by Jenkins needs-attention Indicates a PR validation failure (set by CI infrastructure) and removed needs-attention Indicates a PR validation failure (set by CI infrastructure) validating PR is currently being validated by Jenkins labels Jun 17, 2017
@akka-ci

akka-ci commented Jun 17, 2017

Copy link
Copy Markdown

Test FAILed.

@akka-ci akka-ci added the needs-attention Indicates a PR validation failure (set by CI infrastructure) label Jun 17, 2017
@akka-ci

akka-ci commented Jun 17, 2017

Copy link
Copy Markdown

Test FAILed.

@agolubev

Copy link
Copy Markdown
Contributor Author

build failed due to binary incompatible - not sure how to fix this so far

@akka-ci akka-ci added validating PR is currently being validated by Jenkins tested PR that was successfully built and tested by Jenkins and removed needs-attention Indicates a PR validation failure (set by CI infrastructure) validating PR is currently being validated by Jenkins labels Jun 22, 2017
@akka-ci

akka-ci commented Jun 22, 2017

Copy link
Copy Markdown

Test PASSed.

@agolubev

Copy link
Copy Markdown
Contributor Author

Ref #23078

@patriknw patriknw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I need to think some more about this. So far only detailed comments without understanding if there is a better solution.

Source.queue[Unit](10, OverflowStrategy.fail).toMat(TestSink.probe)(Keep.both).run()
intercept[IllegalStateException] {
Await.result(
(1 to 15).map(_ ⇒ queue.offer(())).last, 3.seconds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this guaranteed to throw?
I think you have to use a TestSink that is not requesting elements to be sure?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, I use TestSink.probe in line 297. So I believe this is what you are expecting.

val out = Outlet[T]("queueSource.out")
val out = Outlet[T]("queueSource.offerout")
override val shape: SourceShape[T] = SourceShape.of(out)
println(QueueSource)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remember to remove

abstract class ConcurrentGraphStageLogic[T] private[stream] (override val inCount: Int, override val outCount: Int) extends GraphStageLogic(inCount, outCount) {
def this(shape: Shape) = this(shape.inlets.size, shape.outlets.size)

val concurrentSet = ConcurrentHashMap.newKeySet[Promise[_]]()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private?

*/
private[this] final val lock = new ReentrantLock

private[this] val callbackState = new AtomicReference[CallbackState](NotInitialized(Nil))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only accessed inside the lock so then there shouldn't be any need for an AtomicReference.


ac.ack.map(ack ⇒ {
ack.future.andThen { case _ ⇒ concurrentSet.remove(ack) }(ec)
concurrentSet.add(ack)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

place this above previous line, so that things are not optimized and remove happens before add

}

@throws(classOf[Exception])
override def postStop(): Unit = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

easy to forget calling super.postStop from subclass


override def postStop(): Unit = stopCallback {
override def postStop(): Unit = stopWithStoppedCallbackLogic {
case Pull(promise) ⇒ promise.failure(new IllegalStateException("Stream is terminated. QueueSink is detached"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need for tryFailure?

* To preserve message order when switching between not initialized / initialized states
* lock is used. Case is similar to RepointableActorRef
*/
private[this] final val lock = new ReentrantLock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer synchronized if we don't use any specific features from ReentrantLock


@throws(classOf[Exception])
override def postStop(): Unit = {
concurrentSet.asScala.foreach(_.failure(new IllegalStateException("Stream is stopped. Callback is detached.")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need tryFailure?

@patriknw

Copy link
Copy Markdown
Contributor

Looking at how ordinary AsyncCallback is executed

  • onAsyncInput in GraphInterpreter (ActorGraphInterpreter)
  • new AsyncInput
  • runAsyncInput

runAsyncInput seems to be the place where the event (callback parameter) is applied to the handler, and that is skipped if isStageCompleted.

Could we introduce a

trait HasCallbackPromise[T] {
  def done: Promise[T]
}

and in runAsyncInput fail that promise if isStageCompleted and the event implements that trait ?

What am I missing?

@agolubev

Copy link
Copy Markdown
Contributor Author

It's a little bit more complex. Source.queue returns futures for each element. Future succeeded when an element is pulling by a stream. The issue here is that some data elements already went through runAsyncInput and waiting to be processed by the actor. At this particular time actor finishes because of stream completion. In this situation, some Futures will not be completed at all. This is what I'm trying to solve here.
So we need some buffer of such "in transit" elements. Callback that will remove element once it consumed by actor and some graceful completion that fails all "in transit" futures.

@agolubev

Copy link
Copy Markdown
Contributor Author

Still I can try "HasCallbackPromise as trait" approach instead of "AckedCallback abstract class" it you think this will give some benefits.

@patriknw

patriknw commented Jul 2, 2017

Copy link
Copy Markdown
Contributor

Isn't another issue that it's not safe to return a callback as materialized value, because it can be inoked "too early". That is what the calback wrapper is trying to solve.

@agolubev

agolubev commented Jul 2, 2017

Copy link
Copy Markdown
Contributor Author

@patriknw , callback wrapper solves the issue of invoking materialized value too early - current PR is enhancing wrapper by adding tracking of what messages being processed by stream to complete all futures

@patriknw

patriknw commented Jul 2, 2017

Copy link
Copy Markdown
Contributor

Right, Optimal solution would be to have callbacks that "just works", and not a special one for when you use them as materialized value. This is difficult and it's great that we start thinking about how to solve this.

@agolubev agolubev force-pushed the 23111-AsyncCallbacks_lost_finished_stage-agolubev branch from ca8285c to 1c5fe18 Compare August 7, 2017 22:00
@akka-ci akka-ci added validating PR is currently being validated by Jenkins needs-attention Indicates a PR validation failure (set by CI infrastructure) and removed tested PR that was successfully built and tested by Jenkins labels Aug 7, 2017
@akka-ci akka-ci added validating PR is currently being validated by Jenkins and removed needs-attention Indicates a PR validation failure (set by CI infrastructure) labels Oct 31, 2017
@agolubev

Copy link
Copy Markdown
Contributor Author

rebased and seems like added all feedback changes.

@akka-ci akka-ci added tested PR that was successfully built and tested by Jenkins and removed validating PR is currently being validated by Jenkins labels Oct 31, 2017
@akka-ci

akka-ci commented Oct 31, 2017

Copy link
Copy Markdown

Test PASSed.
5779 tests run, 458 skipped, 0 failed.
Test PASSed.

@akka-ci

akka-ci commented Oct 31, 2017

Copy link
Copy Markdown

Test PASSed.

@patriknw patriknw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with 2 small things

ProblemFilters.exclude[MissingClassProblem]("akka.stream.stage.CallbackWrapper$Initialized$")
ProblemFilters.exclude[MissingClassProblem]("akka.stream.stage.CallbackWrapper$NotInitialized$")
ProblemFilters.exclude[MissingClassProblem]("akka.stream.stage.CallbackWrapper$CallbackState")
ProblemFilters.exclude[MissingClassProblem]("akka.stream.stage.CallbackWrapper") No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file should now be for 2.5.6, and something was added there. Should this be removed?

// not started yet
case list @ Pending(l) ⇒ if (!currentState.compareAndSet(list, Pending(Event(event, OptionVal.None) :: l))) internalInvoke(event)
// started - can just send message to stream
case Initialized ⇒ onAsyncInput(event)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this case to the top

@agolubev agolubev force-pushed the 23111-AsyncCallbacks_lost_finished_stage-agolubev branch from e672e45 to 9b43ce7 Compare October 31, 2017 14:25
@akka-ci akka-ci added validating PR is currently being validated by Jenkins and removed tested PR that was successfully built and tested by Jenkins labels Oct 31, 2017
@agolubev

Copy link
Copy Markdown
Contributor Author

pushed changes per recent feedback

@akka-ci akka-ci added tested PR that was successfully built and tested by Jenkins and removed validating PR is currently being validated by Jenkins labels Oct 31, 2017
@akka-ci

akka-ci commented Oct 31, 2017

Copy link
Copy Markdown

Test PASSed.
5780 tests run, 458 skipped, 0 failed.
Test PASSed.

@akka-ci

akka-ci commented Oct 31, 2017

Copy link
Copy Markdown

Test PASSed.

@johanandren johanandren left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, excellent work @agolubev !

@patriknw patriknw merged commit 3c7d40b into akka:master Nov 9, 2017
@patriknw

patriknw commented Nov 9, 2017

Copy link
Copy Markdown
Contributor

Yay, great work @agolubev
I think this unlocks several other tickets.

@jrudolph jrudolph left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems I missed the train on reviewing soon enough...

It seems that I seem to be missing something. Where are the promises ever completed? I think we also need tests for the new features. A few smaller comments below.

/**
* Dispatch an asynchronous notification.
* This method is thread-safe and may be invoked from external execution contexts.
* Promise in `HasCallbackPromise` will fail if stream is already closed or closed before

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems, the description is based on an outdated version that still had HasCallbackPromise.

(currentState.getAndSet(Initializing): @unchecked) match {
case Pending(l) ⇒ l.reverse.foreach(ack ⇒ {
onAsyncInput(ack.e)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding @unchecked, we usually add a catch-all clause that throws an IllegalStageException to make it explicit that missing a state was not an oversight.

interpreter.onAsyncInput(GraphStageLogic.this, event, handler.asInstanceOf[Any ⇒ Unit])
val result = new ConcurrentAsyncCallback[T](handler)
asyncCallbacksInProgress.add(result)
if (_interpreter != null) result.onStart()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it prevented (or does it need to be prevented) that onStart is called twice, once here, and once below in beforePreStart? Is it the assumption, that if _interpreter != null we currently are executing preStart? But wouldn't then the onStart triggered in beforePreStart already be called?

case list @ Pending(l) ⇒ if (!currentState.compareAndSet(list, Pending(Event(event, OptionVal.None) :: l))) internalInvoke(event)
// initializing is in progress in another thread (initializing thread is managed by akka)
case Initializing ⇒ if (!currentState.compareAndSet(Initializing, Pending(Event(event, OptionVal.None) :: Nil))) {
(currentState.get(): @unchecked) match {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of not just recursing directly? Isn't it now missing matching for Completed?

// started - can just send message to stream
case Initialized ⇒ sendEvent(event, promise)
// initializing is in progress in another thread (initializing thread is managed by Akka)
case Initializing ⇒ if (!currentState.compareAndSet(Initializing, Pending(Event(event, OptionVal(promise)) :: Nil))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below, why not recurse directly?


// external call
override def invokeWithFeedback(event: T): Future[Done] = {
val promise: Promise[Done] = Promise[Done]()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the promise ever completed? Isn't that missing?

Arkatufus added a commit to Arkatufus/akka.net that referenced this pull request Mar 27, 2025
Aaronontheweb added a commit to akkadotnet/akka.net that referenced this pull request May 3, 2025
* Add reproduction unit test

* Port akka/akka-core#23970

* Update API Approval list

* Update API Approval list

* Add async callback unit tests

* skip non-implemented feature

* Fix TaskCompletionSource allocation problem

* Code cleanup

* Implement early callback from akka/akka-core#23185

* Make sure early callbacks also get cancelled on stop

* Fix naming and copyright header

* Update API Approval list

* Fix missing feedback callback

* rerun tests

* Implement locks

* cleanup lock code

* extend locking to `_callbackWaitingForInterpreter`

* Fix unit test

* rerun tests

* xml-doc and TBD cleanup

* more TBD and XML-DOC cleanup

* defined `ConcurrentAsyncCallback`

* introduced `ConcurrentAsyncCallback` into `GraphStage`

* fixed all compilation errors

* cleanup hub code

* added API approvals

* cleaned up reverse

---------

Co-authored-by: Gregorius Soedharmo <arkatufus@yahoo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tested PR that was successfully built and tested by Jenkins

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants