Stop() the Adapter

The purpose of this blog posting is to discuss the newly added Adapter.Stop() method. We’ll go over the scenarios where you might need to use it, explain the semantics of this method and offer some suggestions on how it might be used by adapter authors.

There are many cases where the adapter is waiting on external entities to perform some computation or to do some action. For instance, an adapter that reads mouse position is waiting for the user to move the mouse or click a button. It’s also possible that the adapter performs a long running operation (e.g., a write into a source with big latency). In both situations the adapter is written such that it relinquishes the execution thread until something interesting happens (e.g. the user clicks, the I/O finishes). During that time something may cause the query to stop or abort, and the adapter will not be able to respond to that event in a timely manner because it is waiting on something else to return. In order to make the adapter more responsive we are adding an optional notification that enables the engine to tell the adapter that it must stop.

The Adapter class now exposes a virtual method with the signature as below:

 public virtual void Stop();

The important difference between this method and the existing adapter methods is that it is a mere notification, while the Start() and Resume() calls into the adapter represent actual state transitions. The Stop() method is just a way for the runtime to tell the adapter that it must stop, but it is still up to the adapter to do so.

Adapter writers may override this method and process the stop notification. This method is called by the engine in the event of a

  • Query stopping as a result of user calling Query.Stop().
  • Query aborting as a result of another adapter or query operator throwing an exception.

This notification is asynchronous. It can be called while the adapter is in the middle of an Enqueue/Dequeue/Ready/Stopped call, so adapter writers who make use of this notification need to protect their data structures.

The engine will call this method on a worker thread and does not wait for the user to return from it, however if the adapter is suspended it will be resumed after Stop() returns.
If the Stop() implementation throws an exception it will be ignored, because the query is already stopping or aborting.

If an adapter writer chooses to make use of this notification for the reasons outlined earlier, she must take into account the asynchronous nature of this call. It can be called at any time, so the adapter’s data code that calls into other adapter methods must be synchronized.

Let’s assume the following fictitious adapter, enqueueing a CTI every time the user hits Enter. Notice that we can’t really shut it down correctly unless the user does something, which is not acceptable.

 public class UnresponsiveAdapter : TypedPointInputAdapter<Payload>
{
    public override void Resume() { Worker(); }
    public override void Start() { Worker(); }
    public void Worker()
    {
        while (AdapterState != AdapterState.Stopping)
        {
            Console.ReadLine();
            var now = DateTimeOffset.UtcNow;
            if (EnqueueCtiEvent(now) == EnqueueOperationResult.Full)
            {
                Ready();
                return;
            }
        }

        Stopped();
    }
}

If we wanted to start consuming the stop notification so that we don’t block, we’d need to protect the calls to Enqueue, Ready and Stopped with a lock that guarantees that our state is consistent. Notice how our adapter has become able to respond to the stop request of the query without waiting for the user to do anything.

 public class ResponsiveAdapter : TypedPointInputAdapter<Payload>
{
    private object _lock = new object();

    public override void Resume() { Worker(); }
    public override void Start() { Worker(); }
    public void Worker()
    {
        while (AdapterState != AdapterState.Stopping)
        {
            Console.ReadLine();
            var now = DateTimeOffset.UtcNow;
            lock (_lock)
            {
                if (AdapterState != AdapterState.Stopped)
                {
                    if (EnqueueCtiEvent(now) == EnqueueOperationResult.Full)
                    {
                        Ready();
                        return;
                    }
                }
                else
                {
                    break;
                }
            }
        }

        lock (_lock) { if (AdapterState != AdapterState.Stopped) Stopped(); }
    }

    public override void Stop()
    {
        lock (_lock) { if (AdapterState != AdapterState.Stopped) Stopped(); }
    }
}

Of course, this example is a simplification of what might happen with a real-life adapter. You may want to cancel the wait for Enter to be pressed, set some internal state before you call Stopped(), but the emphasis of this sample is on the need for the adapter writer to serialize the interaction with the adapter because the stop notification is consumed asynchronously.

Regards,
Ciprian Gerea