Monday, January 11, 2016

Remote Proxy Pattern and Dart Isolates


It seemed like a good idea at the time.

I would like to experiment with remote proxy implementations of the proxy pattern in Dart. You know what might work for that? Dart isolates. Well, probably not, but it seems like a good idea...

I start by simplifying the Car class, which will serve as the real subject in the pattern, to just an automobile that can drive, stop, and report state:
// Real Subject
class Car implements Automobile {
  String state = 'idle';
  void drive() { state = 'driving'; }
  void stop()  { state = 'idle'; }
}
The proxy subject will then be tasked with communicating with the real subject across isolates. I am unsure exactly how that is going to work, but the proxy subject will need a SendPort, at the very least, to request the real car update itself. So I start with:
// Proxy Subject
class ProxyCar implements Automobile {
  SendPort _s;
  String _state = "???";

  String get state => _state;
  void drive() { _s.send(#drive); }
  void stop() { _s.send(#stop); }
}
Where the SendPort comes from and how the _state private instance variable gets updated are questions that I will try to answer in a bit.

First, I want to establish the other isolate to which the main thread will talk. Like all isolates, I need to accept a SendPort through which it can send information back to the main thread. And I am pretty sure that I need for the main thread to be able to send information to the isolate, so the first thing I do is create a ReceivePort that I can send back:
other(SendPort s) {
  var r = new ReceivePort();
  s.send(r.sendPort);

  // Will establish real car here...
}
OK, back in the main thread, I do the usual isolate dance. I create a ReceivePort so that I can sent its sendPort property into the spawned other() isolate:
main() {
  ProxyCar car;

  var r = new ReceivePort();

  Isolate.
    spawn(other, r.sendPort).

    // Wait for isolate to be ready, then return first message, which is send
    // port back to the isolate
    then((_) => r.first).

    // Create proxy car with receive stream and isolate send port
    then((s) { /** Create proxy car here... **/ }).

    // Will drive and report state here...
}
After spawning the isolate, I wait for the returned Future to complete, indicating that the isolate is ready. Then, I return the first message sent back from other(), which is the SendPort through which I can send messages from main() to other().

Here, I note a problem. If I read the first property, then the receive port's stream is closed. That will cause problems as the ProxyCar tries to receive messages from the receive port. So instead, I convert the ReceivePort to a broadcast stream:
main() {
  ProxyCar car;

  var r = new ReceivePort();
  var receiveStream = r.asBroadcastStream();

  Isolate.
    spawn(other, r.sendPort).

    // Wait for isolate to be ready, then return first message, which is send
    // port back to the isolate
    then((_) => receiveStream.first).

    // Create proxy car with receive stream and isolate send port
    then((s) { car = new ProxyCar(receiveStream, s); }).

    // Will drive and report state here...
}
With that, I think I see a first pass implementation for the proxy car—it needs a stream on which to listen for messages from the real car and a send port through which messages can be sent to the real car. Something like this should work:
// Proxy Subject
class ProxyCar implements Automobile {
  SendPort _s;
  var _r;
  String _state = "???";

  ProxyCar(this._r, this._s) {
    _r.listen((message) {
      print("[ProxyCar] $message");
      _state = message;
    });
  }
  // state, drive, stop declared already...
}
The only messages that will come through that ReceivePort stream will (for now) be state updates, so I assign them to the _state instance variable. The rest is already in place—the already declared state, stop(), and start() methods will send messages through the SendPort supplied to the constructor.

So the rest of the main thread becomes:
  Isolate.
    spawn(other, r.sendPort).

    // Wait for isolate to be ready, then return first message, which is send
    // port back to the isolate
    then((_) => receiveStream.first).

    // Create proxy car with receive stream and isolate send port
    then((s) { car = new ProxyCar(receiveStream, s); }).

    // Drive proxy car, then wait for state message
    then((_) { car.drive(); }).
    then((_) => receiveStream.first).

    // Proxy car state is ready, so print
    then((_) { print("Car is ${car.state}"); }).

    // Stop proxy car, then wait for state message
    then((_) { car.stop(); }).
    then((_) => receiveStream.first).

    // Proxy car state is ready, so print
    then((_) { print("Car is ${car.state}"); });
Last, I need the real subject to work with the opposite send and receive ports. I will follow the same constructor signature, even though a broadcast stream is not necessary in the other isolate. That will make the creation of the real car look like:
other(SendPort s) {
  var r = new ReceivePort();
  s.send(r.sendPort);

  var receiveStream = r.asBroadcastStream();
  new Car(receiveStream, s);
}
And the real class needs to establish a listener for drive and stop messages from the proxy:
class Car implements Automobile {
  SendPort _s;
  var _r;
  Car(this._r, this._s) {
    _r.listen((message) {
      print(message);
      if (message == #drive) drive();
      if (message == #stop)  stop();
      _s.send(state);
    });
  }

  String state = 'idle';
  void drive() { state = 'driving'; }
  void stop()  { state = 'idle'; }
}
Phew! That was a lot harder than I expected. There is almost certainly some cleanup that I can perform. Perhaps the car classes can create their own ReceivePort instances. A little Future improvement is in order. Still, the code works:
$ ./bin/drive.dart                        
Symbol("drive")
[ProxyCar] driving
Car is driving
Symbol("stop")
[ProxyCar] idle
Car is idle
The real subject receives the #drive symbol. Then the proxy subject receives a message that the real subject is driving. Then the output of the current state from the proxy is that the car is driving.

I suppose I should have known that isolate code would get messy like this. Hopefully I can clean it up tomorrow.


Day #61

No comments:

Post a Comment