Repository: dask/dask

Issue:
Get worker count and other info from client.
Something like this would be useful. It could count all currently registered workers.

``` python
class Client():
    ...
    def get_nworkers():
        # return number of workers.
```

Another useful method could be `Client.count_active_workers` which would ping all workers, then return how many responded after 1 second or so.



Discussion:
Would we want to expose the full `Scheduler.workers` dict?  If so then that would be one protocol back and forth and then lots of simple functions client-side.

Could we use heartbeats (which has other uses) plus sending the `workers` dict (which might hold `"last-seen"` values) rather than making a special protocol to ping all workers and see how many respond in a second?

For maintenance concerns I'm trying to find the fewest possible protocols that can cover our needs.

Maybe `Client.registered_workers` should be a property that gets the `Scheduler.workers` dict?

Yeah, I think that it would be good to expose getting an updated workers dict.  I can think of a few use cases for this so I think it makes sense to peel this off from Client registration.

I heard once that it's good form to avoid using properties with things that are expensive and use explicit functions like `get_registered_workers()` instead.  I guess the idea was that properties look like fields which make the promise of cheap access.


Resolving patch:
diff --git a/dask/distributed/client.py b/dask/distributed/client.py
--- a/dask/distributed/client.py
+++ b/dask/distributed/client.py
@@ -129,3 +129,8 @@ def register_client(self):
         self.send_to_scheduler({'function': 'register'}, {'pid': os.getpid()})
         header, payload = self.recv_from_scheduler()
         self.registered_workers = payload['workers']
+
+    def get_registered_workers(self):
+        self.send_to_scheduler({'function': 'get_workers'}, {})
+        header, payload = self.recv_from_scheduler()
+        return payload['workers']
diff --git a/dask/distributed/scheduler.py b/dask/distributed/scheduler.py
--- a/dask/distributed/scheduler.py
+++ b/dask/distributed/scheduler.py
@@ -135,6 +135,7 @@ def __init__(self, port_to_workers=None, port_to_clients=None,
                                  'setitem-ack': self._setitem_ack,
                                  'getitem-ack': self._getitem_ack}
         self.client_functions = {'status': self._status_to_client,
+                                 'get_workers': self._get_workers,
                                  'register': self._client_registration,
                                  'schedule': self._schedule_from_client,
                                  'set-collection': self._set_collection,
@@ -704,3 +705,10 @@ def _get_collection(self, header, payload):
                        'dumps': dill.dumps}
 
             self.send_to_client(header['address'], header2, payload2)
+
+    def _get_workers(self, header, payload):
+        with logerrors():
+            log(self.address_to_clients, "Get workers", header)
+            self.send_to_client(header['address'],
+                                {'status': 'OK'},
+                                {'workers': self.workers})


Test patch:
diff --git a/dask/distributed/tests/test_client.py b/dask/distributed/tests/test_client.py
--- a/dask/distributed/tests/test_client.py
+++ b/dask/distributed/tests/test_client.py
@@ -121,3 +121,14 @@ def test_register_with_scheduler():
         assert s.clients[c.address]['pid'] == os.getpid()
         assert (c.registered_workers == {a.address: {'pid': pid},
                                          b.address: {'pid': pid}})
+
+
+def test_get_workers():
+    with scheduler_and_workers() as (s, (a, b)):
+        c = Client(s.address_to_clients)
+        pid = os.getpid()
+        assert c.get_registered_workers() == {a.address: {'pid': pid},
+                                              b.address: {'pid': pid}}
+
+        s.close_workers()
+        assert c.get_registered_workers() == {}