Repository: dask/distributed

Issue:
Yank state machine out of Worker class
This issue is a tracker for the worker state machine refactor, as described in the wider epic #5736.

In scope for this issue is to yank out all state machine logic from Worker and create two new classes, WorkerState and BaseWorker.

Writing additional unit tests, beyond tweaking the already existing ones, is out of scope.

Contributing PRs:
- #6579
Blocked by:
  - #6581 
  - #6566
  Blocked by:
    - #5896
    - #6410 
    - #6475
    Blocked by:
      - #6474 
    - #6512 
    - #6564


Discussion:
apparently _blocks_ also acts as a "please close ticket" keyword 🤔 
Just wanted to leave some words of gratitude and encouragement. It's great to see this move forward; memory leaks and deadlocks due to worker transitions have become a real problem in our production code lately, so your work on this is greatly appreciated!
I'll have to move `_notify_plugins` from Worker to WorkerState and I'll have to share the `Worker.plugins` dict by reference with WorkerState. This is because of
https://github.com/dask/distributed/blob/9b8172b56c749eab621d69e747ed76bf2841d0f2/distributed/worker.py#L2783

@fjetter were you expecting this? Do you have an alternative design in mind?
I'm going to apply the following changes to methods and attributes of Worker. This will potentially break third party code that directly interact with (what we believe is) internal API.
Please review and speak up if you see any issues.

- These attributes will be shared by reference between Worker and WorkerState, like it already happens for `WorkerMemoryManager`:
  - `data`
  - `plugins`
  - `tasks`
 
- These methods will move from `Worker` to `Worker.state` and will become private, with no deprecation cycle:
  - `transitions()`
  - `transition_<begin>_<finish>()`
  - `ensure_task_exists()`
  - `validate_task_<state>()`
- These attributes and methods will move from `Worker` to `Worker.state`, with an accessor in Worker which will raise a DeprecationWarning:
  - `tasks`
  - `log`
  - `stimulus_log`
  - `story()`
  - `stimulus_story()`
  - `total_out_connections`
  - `comm_threshold_bytes`
  - `data_needed`
  - `data_needed_per_worker`
  - `ready`
  - `constrained`
  - `executing_count`
  - `executed_count`
  - `long_running`
  - `has_what`
  - `in_flight_tasks`
  - `in_flight_workers`
  - `busy_workers`
  - `waiting_for_data_count`
  - `generation`
  - `target_message_size`
  - `transition_counter`
  - `transition_counter_max`
  - `available_resources`
  - `_missing_dep_flight`
  - `_executing`
  - `executing_count` property
  - `_in_flight_tasks`
  - `in_flight_tasks` property
  - `validate_task()`
This list looks good to me.

Resolving patch:
diff --git a/distributed/dashboard/components/worker.py b/distributed/dashboard/components/worker.py
--- a/distributed/dashboard/components/worker.py
+++ b/distributed/dashboard/components/worker.py
@@ -88,10 +88,10 @@ def update(self):
         w = self.worker
         d = {
             "Stored": [len(w.data)],
-            "Executing": ["%d / %d" % (w.executing_count, w.nthreads)],
-            "Ready": [len(w.ready)],
-            "Waiting": [w.waiting_for_data_count],
-            "Connections": [len(w.in_flight_workers)],
+            "Executing": ["%d / %d" % (w.state.executing_count, w.state.nthreads)],
+            "Ready": [len(w.state.ready)],
+            "Waiting": [w.state.waiting_for_data_count],
+            "Connections": [len(w.state.in_flight_workers)],
             "Serving": [len(w._comms)],
         }
         update(self.source, d)
@@ -225,7 +225,7 @@ def __init__(self, worker, **kwargs):
         fig = figure(
             title="Communication History",
             x_axis_type="datetime",
-            y_range=[-0.1, worker.total_out_connections + 0.5],
+            y_range=[-0.1, worker.state.total_out_connections + 0.5],
             height=150,
             tools="",
             x_range=x_range,
@@ -247,7 +247,7 @@ def update(self):
             {
                 "x": [time() * 1000],
                 "out": [len(self.worker._comms)],
-                "in": [len(self.worker.in_flight_workers)],
+                "in": [len(self.worker.state.in_flight_workers)],
             },
             10000,
         )
@@ -263,7 +263,7 @@ def __init__(self, worker, **kwargs):
         fig = figure(
             title="Executing History",
             x_axis_type="datetime",
-            y_range=[-0.1, worker.nthreads + 0.1],
+            y_range=[-0.1, worker.state.nthreads + 0.1],
             height=150,
             tools="",
             x_range=x_range,
@@ -281,7 +281,7 @@ def __init__(self, worker, **kwargs):
     @log_errors
     def update(self):
         self.source.stream(
-            {"x": [time() * 1000], "y": [self.worker.executing_count]}, 1000
+            {"x": [time() * 1000], "y": [self.worker.state.executing_count]}, 1000
         )
 
 
diff --git a/distributed/http/worker/prometheus/core.py b/distributed/http/worker/prometheus/core.py
--- a/distributed/http/worker/prometheus/core.py
+++ b/distributed/http/worker/prometheus/core.py
@@ -27,21 +27,21 @@ def collect(self):
             labels=["state"],
         )
         tasks.add_metric(["stored"], len(self.server.data))
-        tasks.add_metric(["executing"], self.server.executing_count)
-        tasks.add_metric(["ready"], len(self.server.ready))
-        tasks.add_metric(["waiting"], self.server.waiting_for_data_count)
+        tasks.add_metric(["executing"], self.server.state.executing_count)
+        tasks.add_metric(["ready"], len(self.server.state.ready))
+        tasks.add_metric(["waiting"], self.server.state.waiting_for_data_count)
         yield tasks
 
         yield GaugeMetricFamily(
             self.build_name("concurrent_fetch_requests"),
             "Number of open fetch requests to other workers.",
-            value=len(self.server.in_flight_workers),
+            value=len(self.server.state.in_flight_workers),
         )
 
         yield GaugeMetricFamily(
             self.build_name("threads"),
             "Number of worker threads.",
-            value=self.server.nthreads,
+            value=self.server.state.nthreads,
         )
 
         yield GaugeMetricFamily(
diff --git a/distributed/worker_state_machine.py b/distributed/worker_state_machine.py
--- a/distributed/worker_state_machine.py
+++ b/distributed/worker_state_machine.py
@@ -8,6 +8,7 @@
 import operator
 import random
 import sys
+import warnings
 import weakref
 from collections import defaultdict, deque
 from collections.abc import (
@@ -3194,12 +3195,11 @@ def __set_name__(self, owner: type, name: str) -> None:
         self.name = name
 
     def _warn_deprecated(self) -> None:
-        pass
-        # warnings.warn(
-        #     f"The `Worker.{self.name}` attribute has been moved to "
-        #     f"`Worker.state.{self.target or self.name}",
-        #     FutureWarning,
-        # )
+        warnings.warn(
+            f"The `Worker.{self.name}` attribute has been moved to "
+            f"`Worker.state.{self.target or self.name}`",
+            FutureWarning,
+        )
 
     def __get__(self, instance: Worker | None, _):
         if instance is None:


Test patch:
diff --git a/distributed/dashboard/tests/test_scheduler_bokeh.py b/distributed/dashboard/tests/test_scheduler_bokeh.py
--- a/distributed/dashboard/tests/test_scheduler_bokeh.py
+++ b/distributed/dashboard/tests/test_scheduler_bokeh.py
@@ -121,8 +121,8 @@ async def test_stealing_events(c, s, a, b):
     await wait(futures)
     se.update()
     assert len(first(se.source.data.values()))
-    assert b.tasks
-    assert sum(se.source.data["count"]) >= len(b.tasks)
+    assert b.state.tasks
+    assert sum(se.source.data["count"]) >= len(b.state.tasks)
 
 
 @gen_cluster(client=True)
@@ -133,7 +133,7 @@ async def test_events(c, s, a, b):
         slowinc, range(100), delay=0.1, workers=a.address, allow_other_workers=True
     )
 
-    while not b.tasks:
+    while not b.state.tasks:
         await asyncio.sleep(0.01)
 
     e.update()
diff --git a/distributed/deploy/tests/test_local.py b/distributed/deploy/tests/test_local.py
--- a/distributed/deploy/tests/test_local.py
+++ b/distributed/deploy/tests/test_local.py
@@ -90,7 +90,7 @@ def test_procs():
         assert len(c.workers) == 2
         assert all(isinstance(w, Worker) for w in c.workers.values())
         with Client(c.scheduler.address) as e:
-            assert all(w.nthreads == 3 for w in c.workers.values())
+            assert all(w.state.nthreads == 3 for w in c.workers.values())
             assert all(isinstance(w, Worker) for w in c.workers.values())
         repr(c)
 
@@ -324,7 +324,7 @@ async def test_defaults_2():
         dashboard_address=":0",
         asynchronous=True,
     ) as c:
-        assert sum(w.nthreads for w in c.workers.values()) == CPU_COUNT
+        assert sum(w.state.nthreads for w in c.workers.values()) == CPU_COUNT
         assert all(isinstance(w, Worker) for w in c.workers.values())
         assert len(c.workers) == 1
 
diff --git a/distributed/deploy/tests/test_spec_cluster.py b/distributed/deploy/tests/test_spec_cluster.py
--- a/distributed/deploy/tests/test_spec_cluster.py
+++ b/distributed/deploy/tests/test_spec_cluster.py
@@ -42,9 +42,9 @@ async def test_specification():
         assert isinstance(cluster.workers[1], Worker)
         assert isinstance(cluster.workers["my-worker"], MyWorker)
 
-        assert cluster.workers[0].nthreads == 1
-        assert cluster.workers[1].nthreads == 2
-        assert cluster.workers["my-worker"].nthreads == 3
+        assert cluster.workers[0].state.nthreads == 1
+        assert cluster.workers[1].state.nthreads == 2
+        assert cluster.workers["my-worker"].state.nthreads == 3
 
         async with Client(cluster, asynchronous=True) as client:
             result = await client.submit(lambda x: x + 1, 10)
@@ -69,9 +69,9 @@ def test_spec_sync(loop):
         assert isinstance(cluster.workers[1], Worker)
         assert isinstance(cluster.workers["my-worker"], MyWorker)
 
-        assert cluster.workers[0].nthreads == 1
-        assert cluster.workers[1].nthreads == 2
-        assert cluster.workers["my-worker"].nthreads == 3
+        assert cluster.workers[0].state.nthreads == 1
+        assert cluster.workers[1].state.nthreads == 2
+        assert cluster.workers["my-worker"].state.nthreads == 3
 
         with Client(cluster, loop=loop) as client:
             assert cluster.loop is cluster.scheduler.loop
diff --git a/distributed/diagnostics/tests/test_worker_plugin.py b/distributed/diagnostics/tests/test_worker_plugin.py
--- a/distributed/diagnostics/tests/test_worker_plugin.py
+++ b/distributed/diagnostics/tests/test_worker_plugin.py
@@ -106,7 +106,7 @@ async def test_normal_task_transitions_called(c, s, w):
 
     await c.register_worker_plugin(plugin)
     await c.submit(lambda x: x, 1, key="task")
-    await async_wait_for(lambda: not w.tasks, timeout=10)
+    await async_wait_for(lambda: not w.state.tasks, timeout=10)
 
 
 @gen_cluster(nthreads=[("127.0.0.1", 1)], client=True)
@@ -148,7 +148,7 @@ async def test_superseding_task_transitions_called(c, s, w):
 
     await c.register_worker_plugin(plugin)
     await c.submit(lambda x: x, 1, key="task", resources={"X": 1})
-    await async_wait_for(lambda: not w.tasks, timeout=10)
+    await async_wait_for(lambda: not w.state.tasks, timeout=10)
 
 
 @gen_cluster(nthreads=[("127.0.0.1", 1)], client=True)
@@ -174,7 +174,7 @@ async def test_dependent_tasks(c, s, w):
 
     await c.register_worker_plugin(plugin)
     await c.get(dsk, "task", sync=False)
-    await async_wait_for(lambda: not w.tasks, timeout=10)
+    await async_wait_for(lambda: not w.state.tasks, timeout=10)
 
 
 @gen_cluster(nthreads=[("127.0.0.1", 1)], client=True)
@@ -207,7 +207,7 @@ class Dummy(WorkerPlugin):
     with warnings.catch_warnings(record=True) as record:
         await c.register_worker_plugin(Dummy())
         assert await c.submit(inc, 1, key="x") == 2
-        while "x" in a.tasks:
+        while "x" in a.state.tasks:
             await asyncio.sleep(0.01)
 
     assert not record
@@ -235,7 +235,7 @@ def teardown(self, worker):
     await c.submit(inc, 0)
     assert w.foo == 123
 
-    while s.tasks or w.tasks:
+    while s.tasks or w.state.tasks:
         await asyncio.sleep(0.01)
 
     class MyCustomPlugin(WorkerPlugin):
diff --git a/distributed/http/scheduler/tests/test_scheduler_http.py b/distributed/http/scheduler/tests/test_scheduler_http.py
--- a/distributed/http/scheduler/tests/test_scheduler_http.py
+++ b/distributed/http/scheduler/tests/test_scheduler_http.py
@@ -144,7 +144,7 @@ async def fetch_metrics():
 
     # submit a task which should show up in the prometheus scraping
     future = c.submit(slowinc, 1, delay=0.5)
-    while not any(future.key in w.tasks for w in [a, b]):
+    while not any(future.key in w.state.tasks for w in [a, b]):
         await asyncio.sleep(0.001)
 
     active_metrics, forgotten_tasks = await fetch_metrics()
@@ -157,7 +157,7 @@ async def fetch_metrics():
 
     future.release()
 
-    while any(future.key in w.tasks for w in [a, b]):
+    while any(future.key in w.state.tasks for w in [a, b]):
         await asyncio.sleep(0.001)
 
     active_metrics, forgotten_tasks = await fetch_metrics()
diff --git a/distributed/tests/test_active_memory_manager.py b/distributed/tests/test_active_memory_manager.py
--- a/distributed/tests/test_active_memory_manager.py
+++ b/distributed/tests/test_active_memory_manager.py
@@ -459,12 +459,12 @@ async def test_drop_with_paused_workers_with_running_tasks_1(c, s, a, b):
     x = (await c.scatter({"x": 1}, broadcast=True))["x"]
     y = c.submit(slowinc, x, delay=2.5, key="y", workers=[a.address])
 
-    while "y" not in a.tasks or a.tasks["y"].state != "executing":
+    while "y" not in a.state.tasks or a.state.tasks["y"].state != "executing":
         await asyncio.sleep(0.01)
     a.status = Status.paused
     while s.workers[a.address].status != Status.paused:
         await asyncio.sleep(0.01)
-    assert a.tasks["y"].state == "executing"
+    assert a.state.tasks["y"].state == "executing"
 
     s.extensions["amm"].run_once()
     await y
@@ -509,7 +509,7 @@ async def test_drop_with_paused_workers_with_running_tasks_3_4(c, s, a, b, pause
     """
     x = (await c.scatter({"x": 1}, broadcast=True))["x"]
     y = c.submit(slowinc, x, delay=2.5, key="y", workers=[a.address])
-    while "y" not in a.tasks or a.tasks["y"].state != "executing":
+    while "y" not in a.state.tasks or a.state.tasks["y"].state != "executing":
         await asyncio.sleep(0.01)
 
     if pause:
@@ -519,7 +519,7 @@ async def test_drop_with_paused_workers_with_running_tasks_3_4(c, s, a, b, pause
             await asyncio.sleep(0.01)
 
     assert s.tasks["y"].state == "processing"
-    assert a.tasks["y"].state == "executing"
+    assert a.state.tasks["y"].state == "executing"
 
     s.extensions["amm"].run_once()
     await y
@@ -544,10 +544,10 @@ async def test_drop_with_paused_workers_with_running_tasks_5(c, s, w1, w2, w3):
 
     def executing() -> bool:
         return (
-            "y1" in w1.tasks
-            and w1.tasks["y1"].state == "executing"
-            and "y2" in w3.tasks
-            and w3.tasks["y2"].state == "executing"
+            "y1" in w1.state.tasks
+            and w1.state.tasks["y1"].state == "executing"
+            and "y2" in w3.state.tasks
+            and w3.state.tasks["y2"].state == "executing"
         )
 
     while not executing():
diff --git a/distributed/tests/test_actor.py b/distributed/tests/test_actor.py
--- a/distributed/tests/test_actor.py
+++ b/distributed/tests/test_actor.py
@@ -78,7 +78,7 @@ async def test_client_actions(s, a, b, direct_to_workers):
 
         assert counter._address == a.address
 
-        assert isinstance(a.actors[counter.key], Counter)
+        assert isinstance(a.state.actors[counter.key], Counter)
         assert s.tasks[counter.key].actor
 
         await asyncio.gather(counter.increment(), counter.increment())
@@ -186,7 +186,7 @@ async def test_gc(c, s, a, b):
     await wait(actor)
     del actor
 
-    while a.actors or b.actors:
+    while a.state.actors or b.state.actors:
         await asyncio.sleep(0.01)
 
 
@@ -200,7 +200,7 @@ async def test_track_dependencies(c, s, a, b):
 
     await asyncio.sleep(0.3)
 
-    assert a.actors or b.actors
+    assert a.state.actors or b.state.actors
 
 
 @gen_cluster(client=True)
@@ -208,7 +208,7 @@ async def test_future(c, s, a, b):
     counter = c.submit(Counter, actor=True, workers=[a.address])
     assert isinstance(counter, Future)
     await wait(counter)
-    assert isinstance(a.actors[counter.key], Counter)
+    assert isinstance(a.state.actors[counter.key], Counter)
 
     counter = await counter
     assert isinstance(counter, Actor)
@@ -364,7 +364,7 @@ def add(n, counter):
     while not done.done():
         assert (
             len([ws for ws in s.workers.values() if ws.processing])
-            <= a.nthreads + b.nthreads
+            <= a.state.nthreads + b.state.nthreads
         )
         await asyncio.sleep(0.01)
 
@@ -430,7 +430,7 @@ def __init__(self, x, y=None):
     actors = c.map(Foo, range(10), y=b, actor=True)
     await wait(actors)
 
-    assert all(len(w.actors) == 2 for w in workers)
+    assert all(len(w.state.actors) == 2 for w in workers)
 
 
 @gen_cluster(client=True, nthreads=[("127.0.0.1", 1)] * 4, Worker=Nanny)
@@ -588,7 +588,7 @@ async def test_worker_actor_handle_is_weakref(c, s, a, b):
     del counter
 
     start = time()
-    while a.actors or b.data:
+    while a.state.actors or b.data:
         await asyncio.sleep(0.1)
         assert time() < start + 30
 
diff --git a/distributed/tests/test_cancelled_state.py b/distributed/tests/test_cancelled_state.py
--- a/distributed/tests/test_cancelled_state.py
+++ b/distributed/tests/test_cancelled_state.py
@@ -17,8 +17,8 @@
 
 
 async def wait_for_cancelled(key, dask_worker):
-    while key in dask_worker.tasks:
-        if dask_worker.tasks[key].state == "cancelled":
+    while key in dask_worker.state.tasks:
+        if dask_worker.state.tasks[key].state == "cancelled":
             return
         await asyncio.sleep(0.005)
     assert False
@@ -77,11 +77,11 @@ def f(ev):
     await ev.set()
     assert await fut == 124  # It would be 3 if the result was copied from b
     del fut
-    while "f1" in a.tasks:
+    while "f1" in a.state.tasks:
         await asyncio.sleep(0.01)
 
     assert_story(
-        a.story("f1"),
+        a.state.story("f1"),
         [
             ("f1", "compute-task", "released"),
             ("f1", "released", "waiting", "waiting", {"f1": "ready"}),
@@ -117,7 +117,7 @@ async def test_worker_stream_died_during_comm(c, s, a, b):
     write_event.set()
 
     await res
-    assert any("receive-dep-failed" in msg for msg in b.log)
+    assert any("receive-dep-failed" in msg for msg in b.state.log)
 
 
 @gen_cluster(client=True, nthreads=[("", 1)])
@@ -161,7 +161,7 @@ def blockable_compute(x, lock):
         await block_compute.release()
         assert await fut2 == 3
 
-        b_story = b.story(fut1.key)
+        b_story = b.state.story(fut1.key)
         assert any("receive-dep-failed" in msg for msg in b_story)
         assert any("cancelled" in msg for msg in b_story)
         assert any("resumed" in msg for msg in b_story)
@@ -193,7 +193,7 @@ async def wait_and_raise(*args, **kwargs):
     # itself would raise a cancelled exception. At this point we're concerned
     # about the worker. The task should transition over error to be eventually
     # forgotten since we no longer hold a ref.
-    while fut.key in w.tasks:
+    while fut.key in w.state.tasks:
         await asyncio.sleep(0.01)
 
     assert await fut2 == 2
@@ -203,7 +203,7 @@ async def wait_and_raise(*args, **kwargs):
     # Everything above this line should be generically true, regardless of
     # refactoring. Below verifies some implementation specific test assumptions
 
-    story = w.story(fut.key)
+    story = w.state.story(fut.key)
     start_finish = [(msg[1], msg[2], msg[3]) for msg in story if len(msg) == 7]
     assert ("executing", "released", "cancelled") in start_finish
     assert ("cancelled", "error", "error") in start_finish
@@ -239,7 +239,7 @@ async def get_data(self, comm, *args, **kwargs):
         # future itself would raise a cancelled exception. At this point we're
         # concerned about the worker. The task should transition over error to
         # be eventually forgotten since we no longer hold a ref.
-        while fut1.key in b.tasks:
+        while fut1.key in b.state.tasks:
             await asyncio.sleep(0.01)
         a.block_get_data = False
         # Everything should still be executing as usual after this
@@ -323,16 +323,16 @@ def block_execution(event, lock):
     )
 
     await executing.wait()
-    assert b.tasks[fut1.key].state == "executing"
+    assert b.state.tasks[fut1.key].state == "executing"
     fut1.release()
-    while b.tasks[fut1.key].state == "executing":
+    while b.state.tasks[fut1.key].state == "executing":
         await asyncio.sleep(0.01)
     await lock_executing.release()
-    while b.tasks:
+    while b.state.tasks:
         await asyncio.sleep(0.01)
 
     assert_story(
-        b.story(fut1.key),
+        b.state.story(fut1.key),
         [
             (fut1.key, "executing", "released", "cancelled", {}),
             (fut1.key, "cancelled", "error", "error", {fut1.key: "released"}),
@@ -362,11 +362,11 @@ def block_execution(event, lock):
     await executing.wait()
     fut2 = c.submit(inc, 1, resources={"A": 1}, key="fut2")
 
-    while fut2.key not in a.tasks:
+    while fut2.key not in a.state.tasks:
         await asyncio.sleep(0.01)
 
     fut1.release()
-    while a.tasks[fut1.key].state == "executing":
+    while a.state.tasks[fut1.key].state == "executing":
         await asyncio.sleep(0.01)
     await lock_executing.release()
 
@@ -417,7 +417,7 @@ async def get_data(self, comm, *args, **kwargs):
 
         await wait_for_state(f3.key, "resumed", w2)
         assert_story(
-            w2.log,
+            w2.state.log,
             [
                 (f3.key, "flight", "released", "cancelled", {}),
                 # ...
@@ -504,7 +504,7 @@ async def release_all_futures():
         assert await f4 == 4 + 2
 
         assert_story(
-            b.story(f3.key),
+            b.state.story(f3.key),
             expect=[
                 (f3.key, "ready", "executing", "executing", {}),
                 (f3.key, "executing", "released", "cancelled", {}),
@@ -518,7 +518,7 @@ async def release_all_futures():
             await f3
 
         assert_story(
-            b.story(f3.key),
+            b.state.story(f3.key),
             expect=[
                 (f3.key, "ready", "executing", "executing", {}),
                 (f3.key, "executing", "released", "cancelled", {}),
diff --git a/distributed/tests/test_client.py b/distributed/tests/test_client.py
--- a/distributed/tests/test_client.py
+++ b/distributed/tests/test_client.py
@@ -5093,9 +5093,9 @@ def f(x):
     results = await c.gather(futures)
     # We dispatch 10 tasks and every task generates 11 more tasks
     # 10 * 11 + 10
-    assert a.executed_count + b.executed_count == 120
-    assert a.executed_count >= 10
-    assert b.executed_count > 0
+    assert a.state.executed_count + b.state.executed_count == 120
+    assert a.state.executed_count >= 10
+    assert b.state.executed_count > 0
 
     assert results == [sum(map(inc, range(10)))] * 10
 
@@ -5362,7 +5362,7 @@ async def test_call_stack_future(c, s, a, b):
     assert all(list(first(result.values())) == [future.key] for result in results)
     assert results[0] == results[1]
     result = results[0]
-    ts = a.tasks.get(future.key)
+    ts = a.state.tasks.get(future.key)
     if ts is not None and ts.state == "executing":
         w = a
     else:
@@ -5377,10 +5377,10 @@ async def test_call_stack_future(c, s, a, b):
 @gen_cluster([("127.0.0.1", 4)] * 2, client=True)
 async def test_call_stack_all(c, s, a, b):
     future = c.submit(slowinc, 1, delay=0.8)
-    while not a.executing_count and not b.executing_count:
+    while not a.state.executing_count and not b.state.executing_count:
         await asyncio.sleep(0.01)
     result = await c.call_stack()
-    w = a if a.executing_count else b
+    w = a if a.state.executing_count else b
     assert list(result) == [w.address]
     assert list(result[w.address]) == [future.key]
     assert "slowinc" in str(result)
@@ -5390,7 +5390,7 @@ async def test_call_stack_all(c, s, a, b):
 async def test_call_stack_collections(c, s, a, b):
     da = pytest.importorskip("dask.array")
     x = da.random.random(100, chunks=(10,)).map_blocks(slowinc, delay=0.5).persist()
-    while not a.executing_count and not b.executing_count:
+    while not a.state.executing_count and not b.state.executing_count:
         await asyncio.sleep(0.001)
     result = await c.call_stack(x)
     assert result
@@ -5400,7 +5400,7 @@ async def test_call_stack_collections(c, s, a, b):
 async def test_call_stack_collections_all(c, s, a, b):
     da = pytest.importorskip("dask.array")
     x = da.random.random(100, chunks=(10,)).map_blocks(slowinc, delay=0.5).persist()
-    while not a.executing_count and not b.executing_count:
+    while not a.state.executing_count and not b.state.executing_count:
         await asyncio.sleep(0.001)
     result = await c.call_stack()
     assert result
diff --git a/distributed/tests/test_cluster_dump.py b/distributed/tests/test_cluster_dump.py
--- a/distributed/tests/test_cluster_dump.py
+++ b/distributed/tests/test_cluster_dump.py
@@ -68,7 +68,7 @@ async def test_cluster_dump_state(c, s, a, b, tmp_path):
     await c.dump_cluster_state(filename, format="msgpack")
 
     scheduler_tasks = list(s.tasks.values())
-    worker_tasks = [t for w in (a, b) for t in w.tasks.values()]
+    worker_tasks = [t for w in (a, b) for t in w.state.tasks.values()]
 
     smem_tasks = [t for t in scheduler_tasks if t.state == "memory"]
     wmem_tasks = [t for t in worker_tasks if t.state == "memory"]
@@ -131,7 +131,7 @@ async def test_cluster_dump_story(c, s, a, b, tmp_path):
     story = dump.worker_story("f1", "f2")
     assert story.keys() == {"f1", "f2"}
     for k, task_story in story.items():
-        assert_story(task_story, a.story(k) + b.story(k))
+        assert_story(task_story, a.state.story(k) + b.state.story(k))
 
 
 @gen_cluster(client=True)
diff --git a/distributed/tests/test_failed_workers.py b/distributed/tests/test_failed_workers.py
--- a/distributed/tests/test_failed_workers.py
+++ b/distributed/tests/test_failed_workers.py
@@ -346,6 +346,7 @@ def __sizeof__(self) -> int:
         return parse_bytes(dask.config.get("distributed.comm.offload")) + 1
 
 
+@pytest.mark.slow
 @gen_cluster(client=True)
 async def test_worker_who_has_clears_after_failed_connection(c, s, a, b):
     """This test is very sensitive to cluster state consistency. Timeouts often
@@ -381,9 +382,9 @@ def sink(*args):
 
         await result_fut
 
-        assert not a.has_what.get(n_worker_address)
+        assert not a.state.has_what.get(n_worker_address)
         assert not any(
-            n_worker_address in s for ts in a.tasks.values() for s in ts.who_has
+            n_worker_address in s for ts in a.state.tasks.values() for s in ts.who_has
         )
 
 
@@ -481,7 +482,7 @@ async def test_forget_data_not_supposed_to_have(c, s, a):
         y = c.submit(inc, x, key="y", workers=[b.address])
 
         await b.in_gather_dep.wait()
-        assert b.tasks["x"].state == "flight"
+        assert b.state.tasks["x"].state == "flight"
 
         x.release()
         y.release()
@@ -489,7 +490,7 @@ async def test_forget_data_not_supposed_to_have(c, s, a):
             await asyncio.sleep(0.01)
 
         b.block_gather_dep.set()
-        while b.tasks:
+        while b.state.tasks:
             await asyncio.sleep(0.01)
 
 
diff --git a/distributed/tests/test_priorities.py b/distributed/tests/test_priorities.py
--- a/distributed/tests/test_priorities.py
+++ b/distributed/tests/test_priorities.py
@@ -55,7 +55,7 @@ async def block_worker(
     else:
         ev = Event()
         clog = c.submit(lambda ev: ev.wait(), ev, key="block_worker")
-        while "block_worker" not in w.tasks:
+        while "block_worker" not in w.state.tasks:
             await asyncio.sleep(0.01)
 
     yield
@@ -69,10 +69,10 @@ async def block_worker(
 
     if pause:
         assert len(s.unrunnable) == ntasks_on_worker
-        assert not w.tasks
+        assert not w.state.tasks
         w.status = Status.running
     else:
-        while len(w.tasks) < ntasks_on_worker:
+        while len(w.state.tasks) < ntasks_on_worker:
             await asyncio.sleep(0.01)
         await ev.set()
         await clog
diff --git a/distributed/tests/test_resources.py b/distributed/tests/test_resources.py
--- a/distributed/tests/test_resources.py
+++ b/distributed/tests/test_resources.py
@@ -64,12 +64,12 @@ async def test_submit_many_non_overlapping_2(c, s, a, b):
 
     while len(a.data) + len(b.data) < 100:
         await asyncio.sleep(0.01)
-        assert a.executing_count <= 2
-        assert b.executing_count <= 1
+        assert a.state.executing_count <= 2
+        assert b.state.executing_count <= 1
 
     await wait(futures)
-    assert a.total_resources == a.available_resources
-    assert b.total_resources == b.available_resources
+    assert a.total_resources == a.state.available_resources
+    assert b.total_resources == b.state.available_resources
 
 
 @gen_cluster(
@@ -227,10 +227,10 @@ async def test_minimum_resource(c, s, a):
 
     while len(a.data) < 30:
         await asyncio.sleep(0.01)
-        assert a.executing_count <= 1
+        assert a.state.executing_count <= 1
 
     await wait(futures)
-    assert a.total_resources == a.available_resources
+    assert a.total_resources == a.state.available_resources
 
 
 @gen_cluster(client=True, nthreads=[("127.0.0.1", 2, {"resources": {"A": 1}})])
@@ -270,16 +270,16 @@ async def test_balance_resources(c, s, a, b):
 async def test_set_resources(c, s, a):
     await a.set_resources(A=2)
     assert a.total_resources["A"] == 2
-    assert a.available_resources["A"] == 2
+    assert a.state.available_resources["A"] == 2
     assert s.workers[a.address].resources == {"A": 2}
 
     future = c.submit(slowinc, 1, delay=1, resources={"A": 1})
-    while a.available_resources["A"] == 2:
+    while a.state.available_resources["A"] == 2:
         await asyncio.sleep(0.01)
 
     await a.set_resources(A=3)
     assert a.total_resources["A"] == 3
-    assert a.available_resources["A"] == 2
+    assert a.state.available_resources["A"] == 2
     assert s.workers[a.address].resources == {"A": 3}
 
 
@@ -323,7 +323,7 @@ async def test_dont_optimize_out(c, s, a, b):
     await c.compute(w, resources={tuple(y.__dask_keys__()): {"A": 1}})
 
     for key in map(stringify, y.__dask_keys__()):
-        assert "executing" in str(a.story(key))
+        assert "executing" in str(a.state.story(key))
 
 
 @pytest.mark.skip(reason="atop fusion seemed to break this")
@@ -342,8 +342,8 @@ async def test_full_collections(c, s, a, b):
     z = df.x + df.y  # some extra nodes in the graph
 
     await c.compute(z, resources={tuple(z.dask): {"A": 1}})
-    assert a.log
-    assert not b.log
+    assert a.state.log
+    assert not b.state.log
 
 
 @pytest.mark.parametrize(
diff --git a/distributed/tests/test_scheduler.py b/distributed/tests/test_scheduler.py
--- a/distributed/tests/test_scheduler.py
+++ b/distributed/tests/test_scheduler.py
@@ -939,7 +939,7 @@ async def test_retire_workers(c, s, a, b):
 
     workers = await s.retire_workers()
     assert list(workers) == [a.address]
-    assert workers[a.address]["nthreads"] == a.nthreads
+    assert workers[a.address]["nthreads"] == a.state.nthreads
     assert list(s.workers) == [b.address]
 
     assert s.workers_to_close() == []
@@ -1601,7 +1601,7 @@ async def test_missing_data_errant_worker(c, s, w1, w2, w3):
         await c.replicate(x, workers=[w1.address, w2.address])
 
         y = c.submit(len, x, workers=w3.address)
-        while not w3.tasks:
+        while not w3.state.tasks:
             await asyncio.sleep(0.001)
         await w1.close()
         await wait(y)
@@ -1736,17 +1736,17 @@ def block(lock):
     await lock.acquire()
     future = c.submit(block, lock, resources={"A": 1})
 
-    while not w.executing_count:
+    while not w.state.executing_count:
         await asyncio.sleep(0.01)
 
     await future.cancel()
     await lock.release()
 
-    while w.executing_count:
+    while w.state.executing_count:
         await asyncio.sleep(0.01)
 
     assert not s.workers[w.address].used_resources["A"]
-    assert w.available_resources == {"A": 1}
+    assert w.state.available_resources == {"A": 1}
 
     await c.submit(inc, 1, resources={"A": 1})
 
@@ -2267,8 +2267,8 @@ def finalizer(*args):
             assert "Couldn't gather 1 keys, rescheduling" in client_logger
 
             assert s.tasks[fin.key].who_has == {s.workers[b.address]}
-            assert a.executed_count == 2
-            assert b.executed_count >= 1
+            assert a.state.executed_count == 2
+            assert b.state.executed_count >= 1
             # ^ leave room for a future switch from `remove_worker` to `retire_workers`
 
     # Ensure that the communication was done via the scheduler, i.e. we actually hit a
@@ -2399,7 +2399,7 @@ async def test_quiet_cluster_round_robin(c, s, a, b):
     await c.submit(inc, 1)
     await c.submit(inc, 2)
     await c.submit(inc, 3)
-    assert a.log and b.log
+    assert a.state.log and b.state.log
 
 
 def test_memorystate():
@@ -3212,10 +3212,10 @@ async def test_computations_futures(c, s, a, b):
 @gen_cluster(client=True, nthreads=[("", 1)])
 async def test_transition_counter(c, s, a):
     assert s.transition_counter == 0
-    assert a.transition_counter == 0
+    assert a.state.transition_counter == 0
     await c.submit(inc, 1)
     assert s.transition_counter > 1
-    assert a.transition_counter > 1
+    assert a.state.transition_counter > 1
 
 
 @pytest.mark.slow
@@ -3239,7 +3239,7 @@ async def test_transition_counter_max_scheduler(c, s, a, b):
 async def test_transition_counter_max_worker(c, s, a):
     # This is set by @gen_cluster; it's False in production
     assert s.transition_counter_max > 0
-    a.transition_counter_max = 1
+    a.state.transition_counter_max = 1
     with captured_logger("distributed.core") as logger:
         fut = c.submit(inc, 2)
         while True:
@@ -3251,7 +3251,7 @@ async def test_transition_counter_max_worker(c, s, a):
 
     assert "TransitionCounterMaxExceeded" in logger.getvalue()
     # Worker state is corrupted. Avoid test failure on gen_cluster teardown.
-    a.validate = False
+    a.state.validate = False
 
 
 @gen_cluster(
@@ -3265,10 +3265,10 @@ async def test_disable_transition_counter_max(c, s, a, b):
     This is the default outside of @gen_cluster.
     """
     assert s.transition_counter_max is False
-    assert a.transition_counter_max is False
+    assert a.state.transition_counter_max is False
     assert await c.submit(inc, 1) == 2
     assert s.transition_counter > 1
-    assert a.transition_counter > 1
+    assert a.state.transition_counter > 1
     s.validate_state()
     a.validate_state()
 
@@ -3292,12 +3292,12 @@ async def test_worker_heartbeat_after_cancel(c, s, *workers):
 
     futs = c.map(slowinc, range(100), delay=0.1)
 
-    while sum(w.executing_count for w in workers) < len(workers):
+    while sum(w.state.executing_count for w in workers) < len(workers):
         await asyncio.sleep(0.001)
 
     await c.cancel(futs)
 
-    while any(w.tasks for w in workers):
+    while any(w.state.tasks for w in workers):
         await asyncio.gather(*(w.heartbeat() for w in workers))
 
 
@@ -3571,7 +3571,7 @@ def block(x, event):
         return x
 
     futs = c.map(block, range(100), event=event)
-    while not a.tasks:
+    while not a.state.tasks:
         await asyncio.sleep(0.1)
 
     await a.close(executor_wait=False)
diff --git a/distributed/tests/test_steal.py b/distributed/tests/test_steal.py
--- a/distributed/tests/test_steal.py
+++ b/distributed/tests/test_steal.py
@@ -150,7 +150,7 @@ async def test_stop_in_flight(c, s, a, b):
 
     assert len(a.data) != num_tasks
     del futs
-    while s.tasks or a.tasks or b.tasks:
+    while s.tasks or a.state.tasks or b.state.tasks:
         await asyncio.sleep(0.1)
 
     futs = c.map(
@@ -160,18 +160,18 @@ async def test_stop_in_flight(c, s, a, b):
     assert len(a.data) == num_tasks
 
     del futs
-    while s.tasks or a.tasks or b.tasks:
+    while s.tasks or a.state.tasks or b.state.tasks:
         await asyncio.sleep(0.1)
     futs = c.map(
         slowinc, range(num_tasks), workers=[a.address], allow_other_workers=True
     )
-    while not len(a.tasks) == num_tasks:
+    while not len(a.state.tasks) == num_tasks:
         await asyncio.sleep(0.01)
-    assert len(b.tasks) == 0
+    assert len(b.state.tasks) == 0
     await steal.start()
     await c.gather(futs)
-    assert len(a.tasks) != num_tasks
-    assert len(b.tasks) != 0
+    assert len(a.state.tasks) != num_tasks
+    assert len(b.state.tasks) != 0
 
 
 @gen_cluster(
@@ -200,7 +200,7 @@ def blocked_task(x, lock):
 
     async with lock:
         first = c.submit(blocked_task, 0, lock, workers=[a.address], key="f-0")
-        while first.key not in a.tasks:
+        while first.key not in a.state.tasks:
             await asyncio.sleep(0.001)
         # Ensure the task is indeed blocked
         with pytest.raises(asyncio.TimeoutError):
@@ -218,10 +218,10 @@ def blocked_task(x, lock):
         )
         # All tasks are put on A since this is what we asked for. Only work
         # stealing should rebalance the tasks once we allow for it
-        while not len(a.tasks) == 11:
+        while not len(a.state.tasks) == 11:
             await asyncio.sleep(0.1)
 
-        assert len(b.tasks) == 0
+        assert len(b.state.tasks) == 0
 
         await steal.start()
         # A is still blocked by executing task f-1 so this can only pass if
@@ -322,7 +322,7 @@ async def test_new_worker_steals(c, s, a):
 
     futures = c.map(slowinc, range(100), delay=0.05)
     total = c.submit(sum, futures)
-    while len(a.tasks) < 10:
+    while len(a.state.tasks) < 10:
         await asyncio.sleep(0.01)
 
     b = await Worker(s.address, nthreads=1, memory_limit=MEMORY_LIMIT)
@@ -364,18 +364,18 @@ async def test_dont_steal_worker_restrictions(c, s, a, b):
 
     futures = c.map(slowinc, range(100), delay=0.1, workers=a.address)
 
-    while len(a.tasks) + len(b.tasks) < 100:
+    while len(a.state.tasks) + len(b.state.tasks) < 100:
         await asyncio.sleep(0.01)
 
-    assert len(a.tasks) == 100
-    assert len(b.tasks) == 0
+    assert len(a.state.tasks) == 100
+    assert len(b.state.tasks) == 0
 
     s.extensions["stealing"].balance()
 
     await asyncio.sleep(0.1)
 
-    assert len(a.tasks) == 100
-    assert len(b.tasks) == 0
+    assert len(a.state.tasks) == 100
+    assert len(b.state.tasks) == 0
 
 
 @gen_cluster(
@@ -388,20 +388,20 @@ async def test_steal_worker_restrictions(c, s, wa, wb, wc):
     ntasks = 100
     futures = c.map(slowinc, range(ntasks), delay=0.1, workers={wa.address, wb.address})
 
-    while sum(len(w.tasks) for w in [wa, wb, wc]) < ntasks:
+    while sum(len(w.state.tasks) for w in [wa, wb, wc]) < ntasks:
         await asyncio.sleep(0.01)
 
-    assert 0 < len(wa.tasks) < ntasks
-    assert 0 < len(wb.tasks) < ntasks
-    assert len(wc.tasks) == 0
+    assert 0 < len(wa.state.tasks) < ntasks
+    assert 0 < len(wb.state.tasks) < ntasks
+    assert len(wc.state.tasks) == 0
 
     s.extensions["stealing"].balance()
 
     await asyncio.sleep(0.1)
 
-    assert 0 < len(wa.tasks) < ntasks
-    assert 0 < len(wb.tasks) < ntasks
-    assert len(wc.tasks) == 0
+    assert 0 < len(wa.state.tasks) < ntasks
+    assert 0 < len(wb.state.tasks) < ntasks
+    assert len(wc.state.tasks) == 0
 
 
 @pytest.mark.skipif(not LINUX, reason="Need 127.0.0.2 to mean localhost")
@@ -411,16 +411,16 @@ async def test_dont_steal_host_restrictions(c, s, a, b):
     await future
 
     futures = c.map(slowinc, range(100), delay=0.1, workers="127.0.0.1")
-    while len(a.tasks) + len(b.tasks) < 100:
+    while len(a.state.tasks) + len(b.state.tasks) < 100:
         await asyncio.sleep(0.01)
-    assert len(a.tasks) == 100
-    assert len(b.tasks) == 0
+    assert len(a.state.tasks) == 100
+    assert len(b.state.tasks) == 0
 
     result = s.extensions["stealing"].balance()
 
     await asyncio.sleep(0.1)
-    assert len(a.tasks) == 100
-    assert len(b.tasks) == 0
+    assert len(a.state.tasks) == 100
+    assert len(b.state.tasks) == 0
 
 
 @pytest.mark.skipif(not LINUX, reason="Need 127.0.0.2 to mean localhost")
@@ -431,22 +431,22 @@ async def test_steal_host_restrictions(c, s, wa, wb):
 
     ntasks = 100
     futures = c.map(slowinc, range(ntasks), delay=0.1, workers="127.0.0.1")
-    while len(wa.tasks) < ntasks:
+    while len(wa.state.tasks) < ntasks:
         await asyncio.sleep(0.01)
-    assert len(wa.tasks) == ntasks
-    assert len(wb.tasks) == 0
+    assert len(wa.state.tasks) == ntasks
+    assert len(wb.state.tasks) == 0
 
     wc = await Worker(s.address, nthreads=1)
 
     start = time()
-    while not wc.tasks or len(wa.tasks) == ntasks:
+    while not wc.state.tasks or len(wa.state.tasks) == ntasks:
         await asyncio.sleep(0.01)
         assert time() < start + 3
 
     await asyncio.sleep(0.1)
-    assert 0 < len(wa.tasks) < ntasks
-    assert len(wb.tasks) == 0
-    assert 0 < len(wc.tasks) < ntasks
+    assert 0 < len(wa.state.tasks) < ntasks
+    assert len(wb.state.tasks) == 0
+    assert 0 < len(wc.state.tasks) < ntasks
 
 
 @gen_cluster(
@@ -457,16 +457,16 @@ async def test_dont_steal_resource_restrictions(c, s, a, b):
     await future
 
     futures = c.map(slowinc, range(100), delay=0.1, resources={"A": 1})
-    while len(a.tasks) + len(b.tasks) < 100:
+    while len(a.state.tasks) + len(b.state.tasks) < 100:
         await asyncio.sleep(0.01)
-    assert len(a.tasks) == 100
-    assert len(b.tasks) == 0
+    assert len(a.state.tasks) == 100
+    assert len(b.state.tasks) == 0
 
     result = s.extensions["stealing"].balance()
 
     await asyncio.sleep(0.1)
-    assert len(a.tasks) == 100
-    assert len(b.tasks) == 0
+    assert len(a.state.tasks) == 100
+    assert len(b.state.tasks) == 0
 
 
 @gen_cluster(client=True, nthreads=[("127.0.0.1", 1, {"resources": {"A": 2}})])
@@ -475,17 +475,17 @@ async def test_steal_resource_restrictions(c, s, a):
     await future
 
     futures = c.map(slowinc, range(100), delay=0.2, resources={"A": 1})
-    while len(a.tasks) < 101:
+    while len(a.state.tasks) < 101:
         await asyncio.sleep(0.01)
-    assert len(a.tasks) == 101
+    assert len(a.state.tasks) == 101
 
     b = await Worker(s.address, nthreads=1, resources={"A": 4})
 
-    while not b.tasks or len(a.tasks) == 101:
+    while not b.state.tasks or len(a.state.tasks) == 101:
         await asyncio.sleep(0.01)
 
-    assert len(b.tasks) > 0
-    assert len(a.tasks) < 101
+    assert len(b.state.tasks) > 0
+    assert len(a.state.tasks) < 101
 
     await b.close()
 
@@ -497,17 +497,17 @@ async def test_steal_resource_restrictions_asym_diff(c, s, a):
     await future
 
     futures = c.map(slowinc, range(100), delay=0.2, resources={"A": 1})
-    while len(a.tasks) < 101:
+    while len(a.state.tasks) < 101:
         await asyncio.sleep(0.01)
-    assert len(a.tasks) == 101
+    assert len(a.state.tasks) == 101
 
     b = await Worker(s.address, nthreads=1, resources={"A": 4, "B": 5})
 
-    while not b.tasks or len(a.tasks) == 101:
+    while not b.state.tasks or len(a.state.tasks) == 101:
         await asyncio.sleep(0.01)
 
-    assert len(b.tasks) > 0
-    assert len(a.tasks) < 101
+    assert len(b.state.tasks) > 0
+    assert len(a.state.tasks) < 101
 
     await b.close()
 
@@ -546,15 +546,15 @@ async def test_dont_steal_executing_tasks_2(c, s, a, b):
     steal = s.extensions["stealing"]
 
     future = c.submit(slowinc, 1, delay=0.5, workers=a.address)
-    while not a.executing_count:
+    while not a.state.executing_count:
         await asyncio.sleep(0.01)
 
     steal.move_task_request(
         s.tasks[future.key], s.workers[a.address], s.workers[b.address]
     )
     await asyncio.sleep(0.1)
-    assert a.tasks[future.key].state == "executing"
-    assert not b.executing_count
+    assert a.state.tasks[future.key].state == "executing"
+    assert not b.state.executing_count
 
 
 @gen_cluster(
@@ -574,7 +574,7 @@ async def test_dont_steal_few_saturated_tasks_many_workers(c, s, a, *rest):
     await wait(futures)
 
     assert len(a.data) == 3
-    assert not any(w.tasks for w in rest)
+    assert not any(w.state.tasks for w in rest)
 
 
 @gen_cluster(
@@ -593,7 +593,7 @@ async def test_steal_when_more_tasks(c, s, a, *rest):
     futures = [c.submit(slowidentity, x, pure=False, delay=0.2) for i in range(20)]
 
     start = time()
-    while not any(w.tasks for w in rest):
+    while not any(w.state.tasks for w in rest):
         await asyncio.sleep(0.01)
         assert time() < start + 1
 
@@ -620,11 +620,11 @@ def slow2(x):
     futures = [c.submit(slowidentity, x, pure=False, delay=0.2) for i in range(10)]
     future = c.submit(slow2, x, priority=-1)
 
-    while not any(w.tasks for w in rest):
+    while not any(w.state.tasks for w in rest):
         await asyncio.sleep(0.01)
 
     # good future moves first
-    assert any(future.key in w.tasks for w in rest)
+    assert any(future.key in w.state.tasks for w in rest)
 
 
 def func(x):
@@ -804,7 +804,7 @@ async def test_steal_twice(c, s, a, b):
     while len(s.tasks) < 100:  # tasks are all allocated
         await asyncio.sleep(0.01)
     # Wait for b to start stealing tasks
-    while len(b.tasks) < 30:
+    while len(b.state.tasks) < 30:
         await asyncio.sleep(0.01)
 
     # Army of new workers arrives to help
@@ -818,11 +818,11 @@ async def test_steal_twice(c, s, a, b):
         len(empty_workers) < 3
     ), f"Too many workers without keys ({len(empty_workers)} out of {len(s.workers)})"
     # This also tests that some tasks were stolen from b
-    # (see `while len(b.tasks) < 30` above)
+    # (see `while len(b.state.tasks) < 30` above)
     assert max(len(ws.has_what) for ws in s.workers.values()) < 30
 
-    assert a.in_flight_tasks == 0
-    assert b.in_flight_tasks == 0
+    assert a.state.in_flight_tasks_count == 0
+    assert b.state.in_flight_tasks_count == 0
 
     await asyncio.gather(*(w.close() for w in workers))
 
@@ -852,12 +852,12 @@ async def test_paused_workers_must_not_steal(c, s, w1, w2, w3):
 async def test_dont_steal_already_released(c, s, a, b):
     future = c.submit(slowinc, 1, delay=0.05, workers=a.address)
     key = future.key
-    while key not in a.tasks:
+    while key not in a.state.tasks:
         await asyncio.sleep(0.05)
 
     del future
 
-    while key in a.tasks and a.tasks[key].state != "released":
+    while key in a.state.tasks and a.state.tasks[key].state != "released":
         await asyncio.sleep(0.05)
 
     a.handle_stimulus(StealRequestEvent(key=key, stimulus_id="test"))
@@ -893,8 +893,8 @@ def long(delay):
         await asyncio.sleep(0.01)
         assert time() < start + 1
 
-    na = a.executing_count
-    nb = b.executing_count
+    na = a.state.executing_count
+    nb = b.state.executing_count
 
     incs = c.map(inc, range(100), workers=a.address, allow_other_workers=True)
 
@@ -904,8 +904,8 @@ def long(delay):
 
     for t in long_tasks:
         assert (
-            sum(log[1] == "executing" for log in a.story(t))
-            + sum(log[1] == "executing" for log in b.story(t))
+            sum(log[1] == "executing" for log in a.state.story(t))
+            + sum(log[1] == "executing" for log in b.state.story(t))
         ) <= 1
 
 
@@ -1052,11 +1052,11 @@ async def test_steal_concurrent_simple(c, s, *workers):
         workers=[w0.address],
     )
 
-    while not w0.tasks:
+    while not w0.state.tasks:
         await asyncio.sleep(0.1)
 
     # ready is a heap but we don't need last, just not the next
-    _, victim_key = w0.ready[-1]
+    _, victim_key = w0.state.ready[-1]
 
     ws0 = s.workers[w0.address]
     ws1 = s.workers[w1.address]
@@ -1087,11 +1087,11 @@ async def test_steal_reschedule_reset_in_flight_occupancy(c, s, *workers):
         range(10),
         key=[f"f1-{ix}" for ix in range(10)],
     )
-    while not w0.tasks:
+    while not w0.state.tasks:
         await asyncio.sleep(0.01)
 
     # ready is a heap but we don't need last, just not the next
-    _, victim_key = w0.ready[-1]
+    _, victim_key = w0.state.ready[-1]
 
     victim_ts = s.tasks[victim_key]
 
@@ -1115,14 +1115,12 @@ async def test_steal_reschedule_reset_in_flight_occupancy(c, s, *workers):
         "distributed.scheduler.work-stealing-interval": 10,
     },
 )
-async def test_get_story(c, s, *workers):
+async def test_get_story(c, s, a, b):
     steal = s.extensions["stealing"]
-    futs = c.map(
-        slowinc, range(100), workers=[workers[0].address], allow_other_workers=True
-    )
+    futs = c.map(slowinc, range(100), workers=[a.address], allow_other_workers=True)
     collect = c.submit(sum, futs)
     await collect
-    key = next(iter(workers[1].tasks))
+    key = next(iter(b.state.tasks))
     ts = s.tasks[key]
     msgs = steal.story(key)
     msgs_ts = steal.story(ts)
@@ -1219,7 +1217,7 @@ async def test_correct_bad_time_estimate(c, s, *workers):
 
 
 @gen_cluster(client=True)
-async def test_steal_stimulus_id_unique(c, s, *workers):
+async def test_steal_stimulus_id_unique(c, s, a, b):
     steal = s.extensions["stealing"]
     num_futs = 1_000
     async with Lock() as lock:
@@ -1229,16 +1227,14 @@ def blocked(x, lock):
 
         # Setup all tasks on worker 0 such that victim/thief relation is the
         # same for all tasks.
-        futures = c.map(
-            blocked, range(num_futs), lock=lock, workers=[workers[0].address]
-        )
+        futures = c.map(blocked, range(num_futs), lock=lock, workers=[a.address])
         # Ensure all tasks are assigned to the worker since otherwise the
         # move_task_request fails.
-        while len(workers[0].tasks) != num_futs:
+        while len(a.state.tasks) != num_futs:
             await asyncio.sleep(0.1)
         tasks = [s.tasks[f.key] for f in futures]
-        w0 = s.workers[workers[0].address]
-        w1 = s.workers[workers[1].address]
+        w0 = s.workers[a.address]
+        w1 = s.workers[b.address]
         # Generating the move task requests as fast as possible increases the
         # chance of duplicates if the uniqueness is not guaranteed.
         for ts in tasks:
diff --git a/distributed/tests/test_stories.py b/distributed/tests/test_stories.py
--- a/distributed/tests/test_stories.py
+++ b/distributed/tests/test_stories.py
@@ -95,7 +95,7 @@ async def test_client_story(c, s, a):
     # If it's a well formed story, we can sort by the last element which is a
     # timestamp and compare the two lists.
     assert sorted(stripped_story, key=lambda msg: msg[-1]) == sorted(
-        s.story(f.key) + a.story(f.key), key=lambda msg: msg[-1]
+        s.story(f.key) + a.state.story(f.key), key=lambda msg: msg[-1]
     )
 
 
@@ -132,11 +132,11 @@ async def test_worker_story_with_deps(c, s, a, b):
     res = c.submit(inc, dep, workers=[b.address], key="res")
     await res
 
-    story = a.story("res")
+    story = a.state.story("res")
     assert story == []
 
     # Story now includes randomized stimulus_ids and timestamps.
-    story = b.story("res")
+    story = b.state.story("res")
     stimulus_ids = {ev[-2].rsplit("-", 1)[0] for ev in story}
     assert stimulus_ids == {"compute-task", "gather-dep-success", "task-finished"}
     # This is a simple transition log
@@ -150,7 +150,7 @@ async def test_worker_story_with_deps(c, s, a, b):
     ]
     assert_story(story, expected, strict=True)
 
-    story = b.story("dep")
+    story = b.state.story("dep")
     stimulus_ids = {ev[-2].rsplit("-", 1)[0] for ev in story}
     assert stimulus_ids == {"compute-task", "gather-dep-success"}
     expected = [
diff --git a/distributed/tests/test_utils_test.py b/distributed/tests/test_utils_test.py
--- a/distributed/tests/test_utils_test.py
+++ b/distributed/tests/test_utils_test.py
@@ -503,7 +503,7 @@ async def test_assert_story_identity(c, s, a, strict):
     assert await f2 == 3
     scheduler_story = s.story(f2.key)
     assert scheduler_story
-    worker_story = a.story(f2.key)
+    worker_story = a.state.story(f2.key)
     assert worker_story
     assert_story(worker_story, worker_story, strict=strict)
     assert_story(scheduler_story, scheduler_story, strict=strict)
@@ -666,7 +666,7 @@ def test_invalid_transitions(capsys):
     async def test_log_invalid_transitions(c, s, a):
         x = c.submit(inc, 1, key="task-name")
         await x
-        ts = a.tasks["task-name"]
+        ts = a.state.tasks["task-name"]
         ev = PauseEvent(stimulus_id="test")
         with mock.patch.object(
             WorkerState, "_handle_event", return_value=({ts: "foo"}, [])
@@ -695,7 +695,7 @@ def test_invalid_worker_state(capsys):
     async def test_log_invalid_worker_task_state(c, s, a):
         x = c.submit(inc, 1, key="task-name")
         await x
-        a.tasks[x.key].state = "released"
+        a.state.tasks[x.key].state = "released"
         with pytest.raises(InvalidTaskState):
             a.validate_state()
 
diff --git a/distributed/tests/test_worker.py b/distributed/tests/test_worker.py
--- a/distributed/tests/test_worker.py
+++ b/distributed/tests/test_worker.py
@@ -98,9 +98,9 @@ async def test_worker_nthreads(s):
 async def test_str(s, a, b):
     assert a.address in str(a)
     assert a.address in repr(a)
-    assert str(a.nthreads) in str(a)
-    assert str(a.nthreads) in repr(a)
-    assert str(a.executing_count) in repr(a)
+    assert str(a.state.nthreads) in str(a)
+    assert str(a.state.nthreads) in repr(a)
+    assert str(a.state.executing_count) in repr(a)
 
 
 @gen_cluster(nthreads=[])
@@ -126,7 +126,7 @@ def __repr__(self):
 
     x = c.submit(NoReprObj, workers=a.address)
     await wait(x)
-    assert not a.executing_count
+    assert not a.state.executing_count
     assert a.data
 
     def bad_func(*args, **kwargs):
@@ -158,7 +158,7 @@ def reset(self):
     y = c.submit(bad_func, x, k=x, workers=b.address)
     await wait(y)
 
-    assert not b.executing_count
+    assert not b.state.executing_count
     assert y.status == "error"
     # Make sure job died because of bad func and not because of bad
     # argument.
@@ -641,7 +641,7 @@ async def test_clean(c, s, a, b):
     await y
 
     collections = [
-        a.tasks,
+        a.state.tasks,
         a.data,
         a.threads,
     ]
@@ -651,7 +651,7 @@ async def test_clean(c, s, a, b):
     x.release()
     y.release()
 
-    while x.key in a.tasks:
+    while x.key in a.state.tasks:
         await asyncio.sleep(0.01)
 
     for c in collections:
@@ -661,8 +661,8 @@ async def test_clean(c, s, a, b):
 @gen_cluster(client=True)
 async def test_message_breakup(c, s, a, b):
     n = 100_000
-    a.target_message_size = 10 * n
-    b.target_message_size = 10 * n
+    a.state.target_message_size = 10 * n
+    b.state.target_message_size = 10 * n
     xs = [
         c.submit(mul, b"%d" % i, n, key=f"x{i}", workers=[a.address]) for i in range(30)
     ]
@@ -678,16 +678,16 @@ async def test_message_breakup(c, s, a, b):
 
 @gen_cluster(client=True)
 async def test_types(c, s, a, b):
-    assert all(ts.type is None for ts in a.tasks.values())
-    assert all(ts.type is None for ts in b.tasks.values())
+    assert all(ts.type is None for ts in a.state.tasks.values())
+    assert all(ts.type is None for ts in b.state.tasks.values())
     x = c.submit(inc, 1, workers=a.address)
     await wait(x)
-    assert a.tasks[x.key].type == int
+    assert a.state.tasks[x.key].type == int
 
     y = c.submit(inc, x, workers=b.address)
     await wait(y)
-    assert b.tasks[x.key].type == int
-    assert b.tasks[y.key].type == int
+    assert b.state.tasks[x.key].type == int
+    assert b.state.tasks[y.key].type == int
 
     await c._cancel(y)
 
@@ -696,7 +696,7 @@ async def test_types(c, s, a, b):
         await asyncio.sleep(0.01)
         assert time() < start + 5
 
-    assert y.key not in b.tasks
+    assert y.key not in b.state.tasks
 
 
 @gen_cluster()
@@ -710,21 +710,21 @@ async def test_system_monitor(s, a, b):
 )
 async def test_restrictions(c, s, a, b):
     # Worker has resource available
-    assert a.available_resources == {"A": 1}
+    assert a.state.available_resources == {"A": 1}
     # Resource restrictions
     x = c.submit(inc, 1, resources={"A": 1})
     await x
-    ts = a.tasks[x.key]
+    ts = a.state.tasks[x.key]
     assert ts.resource_restrictions == {"A": 1}
     await c.cancel([x])
 
     while ts.state == "executing":
         # Resource should be unavailable while task isn't finished
-        assert a.available_resources == {"A": 0}
+        assert a.state.available_resources == {"A": 0}
         await asyncio.sleep(0.01)
 
     # Resource restored after task is in memory
-    assert a.available_resources["A"] == 1
+    assert a.state.available_resources["A"] == 1
 
 
 @gen_cluster(client=True)
@@ -739,8 +739,8 @@ async def test_clean_nbytes(c, s, a, b):
 
     await asyncio.sleep(1)
     assert (
-        len(list(filter(None, [ts.nbytes for ts in a.tasks.values()])))
-        + len(list(filter(None, [ts.nbytes for ts in b.tasks.values()])))
+        len(list(filter(None, [ts.nbytes for ts in a.state.tasks.values()])))
+        + len(list(filter(None, [ts.nbytes for ts in b.state.tasks.values()])))
         == 1
     )
 
@@ -752,7 +752,7 @@ async def test_gather_many_small(c, s, a, *snd_workers, as_deps):
     concurrent outgoing connections. If multiple small fetches from the same worker are
     scheduled all at once, they will result in a single call to gather_dep.
     """
-    a.total_out_connections = 2
+    a.state.total_out_connections = 2
     futures = await c.scatter(
         {f"x{i}": i for i in range(100)},
         workers=[w.address for w in snd_workers],
@@ -767,9 +767,9 @@ async def test_gather_many_small(c, s, a, *snd_workers, as_deps):
         while len(a.data) < 100:
             await asyncio.sleep(0.01)
 
-    assert a.comm_nbytes == 0
+    assert a.state.comm_nbytes == 0
 
-    story = a.story("request-dep", "receive-dep")
+    story = a.state.story("request-dep", "receive-dep")
     assert len(story) == 40  # 1 request-dep + 1 receive-dep per sender worker
     # All GatherDep instructions are fired at the same time; each fetches all keys
     # available on the sender worker
@@ -788,7 +788,7 @@ async def test_multiple_transfers(c, s, w1, w2, w3):
 
     await wait(z)
 
-    r = w3.tasks[z.key].startstops
+    r = w3.state.tasks[z.key].startstops
     transfers = [t for t in r if t["action"] == "transfer"]
     assert len(transfers) == 2
 
@@ -884,7 +884,7 @@ async def test_stop_doing_unnecessary_work(c, s, a, b):
     del futures
 
     start = time()
-    while a.executing_count:
+    while a.state.executing_count:
         await asyncio.sleep(0.01)
         assert time() - start < 0.5
 
@@ -906,7 +906,7 @@ async def test_priorities(c, s, w):
 
     log = [
         t[0]
-        for t in w.log
+        for t in w.state.log
         if t[1] == "executing" and t[2] == "memory" and not t[0].startswith("finalize")
     ]
 
@@ -1476,7 +1476,7 @@ def assert_amm_transfer_story(key: str, w_from: Worker, w_to: Worker) -> None:
     the Active Memory Manager and it was not recalculated on w_to
     """
     assert_story(
-        w_to.story(key),
+        w_to.state.story(key),
         [
             (key, "fetch", "flight", "flight", {}),
             ("request-dep", w_from.address, lambda set_: key in set_),
@@ -1502,11 +1502,13 @@ async def test_close_gracefully(c, s, a, b):
     # be replicated by retire_workers().
     while True:
         mem = {k for k, ts in s.tasks.items() if ts.state == "memory"}
-        if len(mem) >= 8 and any(ts.state == "executing" for ts in b.tasks.values()):
+        if len(mem) >= 8 and any(
+            ts.state == "executing" for ts in b.state.tasks.values()
+        ):
             break
         await asyncio.sleep(0.01)
 
-    assert any(ts for ts in b.tasks.values() if ts.state == "executing")
+    assert any(ts for ts in b.state.tasks.values() if ts.state == "executing")
 
     await b.close_gracefully()
 
@@ -1834,13 +1836,13 @@ async def test_bad_local_directory(s):
 
 @gen_cluster(client=True, nthreads=[])
 async def test_taskstate_metadata(c, s):
-    async with await Worker(s.address) as w:
+    async with await Worker(s.address) as a:
         await c.register_worker_plugin(TaskStateMetadataPlugin())
 
         f = c.submit(inc, 1)
         await f
 
-        ts = w.tasks[f.key]
+        ts = a.state.tasks[f.key]
         assert "start_time" in ts.metadata
         assert "stop_time" in ts.metadata
         assert ts.metadata["stop_time"] > ts.metadata["start_time"]
@@ -1878,9 +1880,9 @@ def f(x):
 async def test_story(c, s, w):
     future = c.submit(inc, 1)
     await future
-    ts = w.tasks[future.key]
-    assert ts.state in str(w.story(ts))
-    assert w.story(ts) == w.story(ts.key)
+    ts = w.state.tasks[future.key]
+    assert ts.state in str(w.state.story(ts))
+    assert w.state.story(ts) == w.state.story(ts.key)
 
 
 @gen_cluster(client=True, nthreads=[("", 1)])
@@ -1891,7 +1893,7 @@ async def test_stimulus_story(c, s, a):
     f2 = c.submit(inc, f1, key="f2")  # This will fail
     await wait([f, f1, f2])
 
-    story = a.stimulus_story("f1", "f2")
+    story = a.state.stimulus_story("f1", "f2")
     assert len(story) == 4
 
     assert isinstance(story[0], ComputeTaskEvent)
@@ -1959,7 +1961,7 @@ async def test_gather_dep_one_worker_always_busy(c, s, a, b):
     f = c.submit(inc, 1, key="f", workers=[a.address])
     g = c.submit(inc, 2, key="g", workers=[a.address])
     await wait([f, g])
-    assert set(a.tasks) == {"f", "g"}
+    assert set(a.state.tasks) == {"f", "g"}
 
     # We will block A for any outgoing communication. This simulates an
     # overloaded worker which will always return "busy" for get_data requests,
@@ -1968,17 +1970,17 @@ async def test_gather_dep_one_worker_always_busy(c, s, a, b):
 
     h = c.submit(add, f, g, key="h", workers=[b.address])
 
-    while h.key not in b.tasks:
+    while h.key not in b.state.tasks:
         await asyncio.sleep(0.01)
 
-    assert b.tasks[h.key].state == "waiting"
-    assert b.tasks[f.key].state in ("flight", "fetch")
-    assert b.tasks[g.key].state in ("flight", "fetch")
+    assert b.state.tasks[h.key].state == "waiting"
+    assert b.state.tasks[f.key].state in ("flight", "fetch")
+    assert b.state.tasks[g.key].state in ("flight", "fetch")
 
     with pytest.raises(asyncio.TimeoutError):
         await h.result(timeout=0.8)
 
-    story = b.story("busy-gather")
+    story = b.state.story("busy-gather")
     # 1 busy response straight away, followed by 1 retry every 150ms for 800ms.
     # The requests for b and g are clustered together in single messages.
     # We need to be very lax in measuring as PeriodicCallback+network comms have been
@@ -2009,7 +2011,7 @@ async def test_gather_dep_local_workers_first(c, s, a, lw, *rws):
     )["f"]
     g = c.submit(inc, f, key="g", workers=[a.address])
     assert await g == 2
-    assert_story(a.story("f"), [("receive-dep", lw.address, {"f"})])
+    assert_story(a.state.story("f"), [("receive-dep", lw.address, {"f"})])
 
 
 @pytest.mark.skipif(not LINUX, reason="Need 127.0.0.2 to mean localhost")
@@ -2033,10 +2035,10 @@ async def test_gather_dep_from_remote_workers_if_all_local_workers_are_busy(
 
     # Tried fetching from each local worker exactly once before falling back to the
     # remote worker
-    assert sorted(ev[1] for ev in a.story("busy-gather")) == sorted(
+    assert sorted(ev[1] for ev in a.state.story("busy-gather")) == sorted(
         w.address for w in lws
     )
-    assert_story(a.story("receive-dep"), [("receive-dep", rw.address, {"f"})])
+    assert_story(a.state.story("receive-dep"), [("receive-dep", rw.address, {"f"})])
 
 
 @gen_cluster(client=True, nthreads=[("127.0.0.1", 1)])
@@ -2250,14 +2252,18 @@ async def assert_task_states_on_worker(expected, worker):
     for _ in range(10):
         try:
             for dep_key, expected_state in expected.items():
-                assert dep_key in worker.tasks, (worker.name, dep_key, worker.tasks)
-                dep_ts = worker.tasks[dep_key]
+                assert dep_key in worker.state.tasks, (
+                    worker.name,
+                    dep_key,
+                    worker.state.tasks,
+                )
+                dep_ts = worker.state.tasks[dep_key]
                 assert dep_ts.state == expected_state, (
                     worker.name,
                     dep_ts,
                     expected_state,
                 )
-            assert set(expected) == set(worker.tasks)
+            assert set(expected) == set(worker.state.tasks)
             return
         except AssertionError as exc:
             active_exc = exc
@@ -2292,12 +2298,12 @@ def raise_exc(*args):
         await res
 
     # Nothing bad happened on B, therefore B should hold on to G
-    assert len(b.tasks) == 1
-    assert g.key in b.tasks
+    assert len(b.state.tasks) == 1
+    assert g.key in b.state.tasks
 
     # A raised the exception therefore we should hold on to the erroneous task
-    assert res.key in a.tasks
-    ts = a.tasks[res.key]
+    assert res.key in a.state.tasks
+    ts = a.state.tasks[res.key]
     assert ts.state == "error"
 
     expected_states = {
@@ -2315,7 +2321,7 @@ def raise_exc(*args):
 
     # We no longer hold any refs to f or g and B didn't have any erros. It
     # releases everything as expected
-    while b.tasks:
+    while b.state.tasks:
         await asyncio.sleep(0.01)
 
     expected_states = {
@@ -2330,9 +2336,8 @@ def raise_exc(*args):
 
     # We no longer hold any refs. Cluster should reset completely
     # This is not happening
-    for server in [s, a, b]:
-        while server.tasks:
-            await asyncio.sleep(0.01)
+    while any((s.tasks, a.state.tasks, b.state.tasks)):
+        await asyncio.sleep(0.01)
 
 
 @gen_cluster(client=True)
@@ -2360,12 +2365,12 @@ def raise_exc(*args):
         await res
 
     # Nothing bad happened on B, therefore B should hold on to G
-    assert len(b.tasks) == 1
-    assert g.key in b.tasks
+    assert len(b.state.tasks) == 1
+    assert g.key in b.state.tasks
 
     # A raised the exception therefore we should hold on to the erroneous task
-    assert res.key in a.tasks
-    ts = a.tasks[res.key]
+    assert res.key in a.state.tasks
+    ts = a.state.tasks[res.key]
     assert ts.state == "error"
 
     expected_states = {
@@ -2383,7 +2388,7 @@ def raise_exc(*args):
     res.release()
     # We no longer hold any refs to f or g and B didn't have any erros. It
     # releases everything as expected
-    while res.key in a.tasks:
+    while res.key in a.state.tasks:
         await asyncio.sleep(0.01)
 
     expected_states = {
@@ -2396,9 +2401,8 @@ def raise_exc(*args):
     f.release()
     g.release()
 
-    for server in [s, a, b]:
-        while server.tasks:
-            await asyncio.sleep(0.01)
+    while any((s.tasks, a.state.tasks, b.state.tasks)):
+        await asyncio.sleep(0.01)
 
 
 @gen_cluster(client=True)
@@ -2426,12 +2430,12 @@ def raise_exc(*args):
         await res
 
     # Nothing bad happened on B, therefore B should hold on to G
-    assert len(b.tasks) == 1
-    assert g.key in b.tasks
+    assert len(b.state.tasks) == 1
+    assert g.key in b.state.tasks
 
     # A raised the exception therefore we should hold on to the erroneous task
-    assert res.key in a.tasks
-    ts = a.tasks[res.key]
+    assert res.key in a.state.tasks
+    ts = a.state.tasks[res.key]
     assert ts.state == "error"
 
     expected_states = {
@@ -2449,7 +2453,7 @@ def raise_exc(*args):
     res.release()
     # We no longer hold any refs to f or g and B didn't have any erros. It
     # releases everything as expected
-    while len(a.tasks) > 1:
+    while len(a.state.tasks) > 1:
         await asyncio.sleep(0.01)
 
     expected_states = {
@@ -2462,9 +2466,8 @@ def raise_exc(*args):
     g.release()
 
     # We no longer hold any refs. Cluster should reset completely
-    for server in [s, a, b]:
-        while server.tasks:
-            await asyncio.sleep(0.01)
+    while any((s.tasks, a.state.tasks, b.state.tasks)):
+        await asyncio.sleep(0.01)
 
 
 @gen_cluster(client=True)
@@ -2546,9 +2549,8 @@ def raise_exc(*args):
     res.release()
 
     # We no longer hold any refs. Cluster should reset completely
-    for server in [s, a, b]:
-        while server.tasks:
-            await asyncio.sleep(0.01)
+    while any((s.tasks, a.state.tasks, b.state.tasks)):
+        await asyncio.sleep(0.01)
 
 
 @gen_cluster(client=True, nthreads=[("", x) for x in (1, 2, 3, 4)])
@@ -2565,10 +2567,10 @@ async def test_hold_on_to_replicas(c, s, *workers):
     f1.release()
     f2.release()
 
-    while sum_2.key not in workers[3].tasks:
+    while sum_2.key not in workers[3].state.tasks:
         await asyncio.sleep(0.01)
 
-    while not workers[3].tasks[sum_2.key].state == "memory":
+    while not workers[3].state.tasks[sum_2.key].state == "memory":
         assert len(s.tasks[f1.key].who_has) >= 2
         assert s.tasks[f2.key].state == "released"
         await asyncio.sleep(0.01)
@@ -2585,15 +2587,15 @@ async def test_forget_dependents_after_release(c, s, a):
 
     await asyncio.wait([fut, fut2])
 
-    assert fut.key in a.tasks
-    assert fut2.key in a.tasks
-    assert fut2.key in {d.key for d in a.tasks[fut.key].dependents}
+    assert fut.key in a.state.tasks
+    assert fut2.key in a.state.tasks
+    assert fut2.key in {d.key for d in a.state.tasks[fut.key].dependents}
 
     fut2.release()
 
-    while fut2.key in a.tasks:
+    while fut2.key in a.state.tasks:
         await asyncio.sleep(0.001)
-    assert fut2.key not in {d.key for d in a.tasks[fut.key].dependents}
+    assert fut2.key not in {d.key for d in a.state.tasks[fut.key].dependents}
 
 
 @gen_cluster(client=True)
@@ -2686,11 +2688,11 @@ async def test_acquire_replicas(c, s, a, b):
 
     for w in (a, b):
         assert w.data[fut.key] == 2
-        assert w.tasks[fut.key].state == "memory"
+        assert w.state.tasks[fut.key].state == "memory"
 
     fut.release()
 
-    while b.tasks or a.tasks:
+    while b.state.tasks or a.state.tasks:
         await asyncio.sleep(0.005)
 
 
@@ -2704,7 +2706,9 @@ async def test_acquire_replicas_same_channel(c, s, a, b):
     s.request_acquire_replicas(b.address, [futA.key], stimulus_id=f"test-{time()}")
 
     await futC
-    while futA.key not in b.tasks or not b.tasks[futA.key].state == "memory":
+    while (
+        futA.key not in b.state.tasks or not b.state.tasks[futA.key].state == "memory"
+    ):
         await asyncio.sleep(0.005)
 
     while len(s.tasks[futA.key].who_has) != 2:
@@ -2715,7 +2719,7 @@ async def test_acquire_replicas_same_channel(c, s, a, b):
 
     for fut in (futA, futB):
         assert_story(
-            b.story(fut.key),
+            b.state.story(fut.key),
             [
                 ("gather-dependencies", a.address, {fut.key}),
                 ("request-dep", a.address, {fut.key}),
@@ -2725,32 +2729,32 @@ async def test_acquire_replicas_same_channel(c, s, a, b):
 
 
 @gen_cluster(client=True, nthreads=[("127.0.0.1", 1)] * 3)
-async def test_acquire_replicas_many(c, s, *workers):
-    futs = c.map(inc, range(10), workers=[workers[0].address])
-    res = c.submit(sum, futs, workers=[workers[1].address])
-    final = c.submit(slowinc, res, delay=0.5, workers=[workers[1].address])
+async def test_acquire_replicas_many(c, s, w1, w2, w3):
+    futs = c.map(inc, range(10), workers=[w1.address])
+    res = c.submit(sum, futs, workers=[w2.address])
+    final = c.submit(slowinc, res, delay=0.5, workers=[w2.address])
 
     await wait(futs)
 
     s.request_acquire_replicas(
-        workers[2].address, [fut.key for fut in futs], stimulus_id=f"test-{time()}"
+        w3.address, [fut.key for fut in futs], stimulus_id=f"test-{time()}"
     )
 
     # Worker 2 should normally not even be involved if there was no replication
     while not all(
-        f.key in workers[2].tasks and workers[2].tasks[f.key].state == "memory"
+        f.key in w3.state.tasks and w3.state.tasks[f.key].state == "memory"
         for f in futs
     ):
         await asyncio.sleep(0.01)
 
-    assert all(ts.state == "memory" for ts in workers[2].tasks.values())
+    assert all(ts.state == "memory" for ts in w3.state.tasks.values())
 
     assert await final == sum(map(inc, range(10))) + 1
     # All workers have a replica
     assert all(len(s.tasks[f.key].who_has) == 3 for f in futs)
     del futs, res, final
 
-    while any(w.tasks for w in workers):
+    while any(w.state.tasks for w in (w1, w2, w3)):
         await asyncio.sleep(0.001)
 
 
@@ -2761,19 +2765,19 @@ async def test_acquire_replicas_already_in_flight(c, s, a):
         x = c.submit(inc, 1, workers=[a.address], key="x")
         y = c.submit(inc, x, workers=[b.address], key="y")
         await b.in_gather_dep.wait()
-        assert b.tasks["x"].state == "flight"
+        assert b.state.tasks["x"].state == "flight"
 
         b.handle_stimulus(
             AcquireReplicasEvent(
                 who_has={"x": a.address}, nbytes={"x": 1}, stimulus_id="test"
             )
         )
-        assert b.tasks["x"].state == "flight"
+        assert b.state.tasks["x"].state == "flight"
         b.block_gather_dep.set()
         assert await y == 3
 
         assert_story(
-            b.story("x"),
+            b.state.story("x"),
             [
                 ("x", "fetch", "flight", "flight", {}),
                 ("x", "flight", "fetch", "flight", {}),
@@ -2798,7 +2802,7 @@ async def test_remove_replicas_simple(c, s, a, b):
 
     assert all(len(s.tasks[f.key].who_has) == 1 for f in futs)
 
-    while b.tasks:
+    while b.state.tasks:
         await asyncio.sleep(0.01)
 
     # Ensure there is no delayed reply to re-register the key
@@ -2823,7 +2827,7 @@ def some_slow(x, event):
     # All interesting things will happen on b
     dependents = c.map(some_slow, futs, event=dependents_event, workers=[b.address])
 
-    while not any(f.key in b.tasks for f in dependents):
+    while not any(f.key in b.state.tasks for f in dependents):
         await asyncio.sleep(0.01)
 
     # The scheduler removes keys from who_has/has_what immediately
@@ -2869,7 +2873,7 @@ def ws_has_futs(aggr_func):
     # reenlist the key in the schedulers state system to avoid race conditions,
     # see also https://github.com/dask/distributed/issues/5265
     rejections = set()
-    for msg in b.log:
+    for msg in b.state.log:
         if msg[0] == "remove-replica-rejected":
             rejections.update(msg[1])
     assert rejections
@@ -2892,7 +2896,9 @@ def answer_sent(key):
         stimulus_id=f"test-{time()}",
     )
 
-    while any(b.tasks[f.key].state != "released" for f in futs if f.key in b.tasks):
+    while any(
+        b.state.tasks[f.key].state != "released" for f in futs if f.key in b.state.tasks
+    ):
         await asyncio.sleep(0.01)
 
     # The scheduler actually gets notified about the removed replica
@@ -2903,7 +2909,7 @@ def answer_sent(key):
 
     del dependents, futs
 
-    while any(w.tasks for w in (a, b)):
+    while any(w.state.tasks for w in (a, b)):
         await asyncio.sleep(0.01)
 
 
@@ -2927,19 +2933,19 @@ async def test_who_has_consistent_remove_replicas(c, s, *workers):
     # suspicious counters are raised since this is expected behaviour when
     # removing replicas
 
-    while f1.key not in a.tasks or a.tasks[f1.key].state != "flight":
+    while f1.key not in a.state.tasks or a.state.tasks[f1.key].state != "flight":
         await asyncio.sleep(0)
 
     coming_from = None
     for w in other_workers:
         coming_from = w
-        if w.address == a.tasks[f1.key].coming_from:
+        if w.address == a.state.tasks[f1.key].coming_from:
             break
 
     coming_from.handle_stimulus(RemoveReplicasEvent(keys=[f1.key], stimulus_id="test"))
     await f2
 
-    assert a.tasks[f1.key].suspicious_count == 0
+    assert a.state.tasks[f1.key].suspicious_count == 0
     assert s.tasks[f1.key].suspicious == 0
 
 
@@ -2953,17 +2959,17 @@ async def test_acquire_replicas_with_no_priority(c, s, a, b):
     y = c.submit(lambda: 2, key="y", workers=[a.address])
     await y
     assert s.tasks["x"].priority is None
-    assert a.tasks["x"].priority is None
+    assert a.state.tasks["x"].priority is None
     assert s.tasks["y"].priority is not None
-    assert a.tasks["y"].priority is not None
+    assert a.state.tasks["y"].priority is not None
 
     s.request_acquire_replicas(b.address, [x.key, y.key], stimulus_id=f"test-{time()}")
     while b.data != {"x": 1, "y": 2}:
         await asyncio.sleep(0.01)
 
     assert s.tasks["x"].priority is None
-    assert a.tasks["x"].priority is None
-    assert b.tasks["x"].priority is not None
+    assert a.state.tasks["x"].priority is None
+    assert b.state.tasks["x"].priority is not None
 
 
 @gen_cluster(client=True, nthreads=[("", 1)])
@@ -2971,7 +2977,7 @@ async def test_acquire_replicas_large_data(c, s, a):
     """When acquire-replicas is used to acquire multiple sizeable tasks, it respects
     target_message_size and acquires them over multiple iterations.
     """
-    size = a.target_message_size // 5 - 10_000
+    size = a.state.target_message_size // 5 - 10_000
 
     class C:
         def __sizeof__(self):
@@ -3003,14 +3009,14 @@ async def test_missing_released_zombie_tasks(c, s, a, b):
     f2 = c.submit(inc, f1, key="f2", workers=[b.address])
     key = f1.key
 
-    while key not in b.tasks or b.tasks[key].state != "fetch":
+    while key not in b.state.tasks or b.state.tasks[key].state != "fetch":
         await asyncio.sleep(0.01)
 
     await a.close()
 
     del f1, f2
 
-    while b.tasks:
+    while b.state.tasks:
         await asyncio.sleep(0.01)
 
 
@@ -3028,10 +3034,10 @@ async def test_missing_released_zombie_tasks_2(c, s, b):
         f1 = c.submit(inc, 1, key="f1", workers=[a.address])
         f2 = c.submit(inc, f1, key="f2", workers=[b.address])
 
-        while f1.key not in b.tasks:
+        while f1.key not in b.state.tasks:
             await asyncio.sleep(0)
 
-        ts = b.tasks[f1.key]
+        ts = b.state.tasks[f1.key]
         assert ts.state == "flight"
 
         while ts.state != "missing":
@@ -3042,11 +3048,11 @@ async def test_missing_released_zombie_tasks_2(c, s, b):
 
         del f1, f2
 
-        while b.tasks:
+        while b.state.tasks:
             await asyncio.sleep(0.01)
 
         assert_story(
-            b.story(ts),
+            b.state.story(ts),
             [("f1", "missing", "released", "released", {"f1": "forgotten"})],
         )
 
@@ -3115,7 +3121,7 @@ async def test_task_flight_compute_oserror(c, s, a, b):
     peer, msg = await write_queue.get()
     assert peer == a.address
     assert msg["op"] == "get_data"
-    in_flight_tasks = [ts for ts in b.tasks.values() if ts.key != "f1"]
+    in_flight_tasks = [ts for ts in b.state.tasks.values() if ts.key != "f1"]
     assert all(ts.state == "flight" for ts in in_flight_tasks)
     await a.close()
     write_event.set()
@@ -3126,7 +3132,7 @@ async def test_task_flight_compute_oserror(c, s, a, b):
     # asserting a few internals to make sure that if things change this is done
     # deliberately
 
-    sum_story = b.story("f1")
+    sum_story = b.state.story("f1")
     expected_sum_story = [
         ("f1", "compute-task", "released"),
         (
@@ -3186,10 +3192,10 @@ async def test_gather_dep_cancelled_rescheduled(c, s):
             await b.in_gather_dep.wait()
 
             fut4.release()
-            while fut4.key in b.tasks:
+            while fut4.key in b.state.tasks:
                 await asyncio.sleep(0)
 
-            assert b.tasks[fut2.key].state == "cancelled"
+            assert b.state.tasks[fut2.key].state == "cancelled"
 
             b.block_gather_dep.set()
             await a.in_get_data.wait()
@@ -3199,7 +3205,7 @@ async def test_gather_dep_cancelled_rescheduled(c, s):
 
             a.block_get_data.set()
             await wait([fut3, fut4])
-            f2_story = b.story(fut2.key)
+            f2_story = b.state.story(fut2.key)
             assert f2_story
 
 
@@ -3221,19 +3227,19 @@ async def test_gather_dep_do_not_handle_response_of_not_requested_tasks(c, s, a)
         fut3 = c.submit(inc, fut1, workers=[b.address], key="f3")
 
         await b.in_gather_dep.wait()
-        assert b.tasks[fut2.key].state == "flight"
+        assert b.state.tasks[fut2.key].state == "flight"
 
         fut4.release()
-        while fut4.key in b.tasks:
+        while fut4.key in b.state.tasks:
             await asyncio.sleep(0.01)
 
-        assert b.tasks[fut2.key].state == "cancelled"
+        assert b.state.tasks[fut2.key].state == "cancelled"
 
         b.block_gather_dep.set()
 
         await fut3
-        assert fut2.key not in b.tasks
-        f2_story = b.story(fut2.key)
+        assert fut2.key not in b.state.tasks
+        f2_story = b.state.story(fut2.key)
         assert f2_story
         assert not any("missing-dep" in msg for msg in f2_story)
 
@@ -3252,17 +3258,17 @@ async def test_gather_dep_no_longer_in_flight_tasks(c, s, a):
         await b.in_gather_dep.wait()
 
         fut2.release()
-        while fut2.key in b.tasks:
+        while fut2.key in b.state.tasks:
             await asyncio.sleep(0.01)
 
-        assert b.tasks[fut1.key].state == "cancelled"
+        assert b.state.tasks[fut1.key].state == "cancelled"
 
         b.block_gather_dep.set()
-        while fut2.key in b.tasks:
+        while fut2.key in b.state.tasks:
             await asyncio.sleep(0.01)
 
-        f1_story = b.story(fut1.key)
-        f2_story = b.story(fut2.key)
+        f1_story = b.state.story(fut1.key)
+        f2_story = b.state.story(fut2.key)
         assert f1_story
         assert f2_story
         assert not any("missing-dep" in msg for msg in f2_story)
diff --git a/distributed/tests/test_worker_memory.py b/distributed/tests/test_worker_memory.py
--- a/distributed/tests/test_worker_memory.py
+++ b/distributed/tests/test_worker_memory.py
@@ -376,12 +376,12 @@ def f(ev):
 
     # Task that is running on the worker when the worker pauses
     x = c.submit(f, ev_x, key="x")
-    while a.executing_count != 1:
+    while a.state.executing_count != 1:
         await asyncio.sleep(0.01)
 
     # Task that is queued on the worker when the worker pauses
     y = c.submit(inc, 1, key="y")
-    while "y" not in a.tasks:
+    while "y" not in a.state.tasks:
         await asyncio.sleep(0.01)
 
     a.status = Status.paused
@@ -403,10 +403,10 @@ def f(ev):
     assert await x == 1
     await asyncio.sleep(0.05)
 
-    assert a.executing_count == 0
-    assert len(a.ready) == 1
-    assert a.tasks["y"].state == "ready"
-    assert "z" not in a.tasks
+    assert a.state.executing_count == 0
+    assert len(a.state.ready) == 1
+    assert a.state.tasks["y"].state == "ready"
+    assert "z" not in a.state.tasks
 
     # Unpause. Tasks that were queued on the worker are executed.
     # Tasks that were stuck on the scheduler are sent to the worker and executed.
@@ -440,13 +440,13 @@ def f(ev):
 
     # Task that is running on the worker when the worker pauses
     x = c.submit(f, ev_x, key="x")
-    while a.executing_count != 1:
+    while a.state.executing_count != 1:
         await asyncio.sleep(0.01)
 
     with captured_logger(logging.getLogger("distributed.worker_memory")) as logger:
         # Task that is queued on the worker when the worker pauses
         y = c.submit(inc, 1, key="y")
-        while "y" not in a.tasks:
+        while "y" not in a.state.tasks:
             await asyncio.sleep(0.01)
 
         # Hog the worker with 900GB unmanaged memory
@@ -470,10 +470,10 @@ def f(ev):
         assert await x == 1
         await asyncio.sleep(0.05)
 
-        assert a.executing_count == 0
-        assert len(a.ready) == 1
-        assert a.tasks["y"].state == "ready"
-        assert "z" not in a.tasks
+        assert a.state.executing_count == 0
+        assert len(a.state.ready) == 1
+        assert a.state.tasks["y"].state == "ready"
+        assert "z" not in a.state.tasks
 
         # Release the memory. Tasks that were queued on the worker are executed.
         # Tasks that were stuck on the scheduler are sent to the worker and executed.
@@ -527,7 +527,7 @@ def pause_on_unpickle():
     # - w and z respectively make x and y go into fetch state.
     #   w has a higher priority than z, therefore w's dependency x has a higher priority
     #   than z's dependency y.
-    #   a.data_needed = ["x", "y"]
+    #   a.state.data_needed = ["x", "y"]
     # - ensure_communicating decides to fetch x but not to fetch y together with it, as
     #   it thinks x is 1TB in size
     # - x fetch->flight; a is added to in_flight_workers
@@ -538,17 +538,17 @@ def pause_on_unpickle():
     # - ensure_communicating is triggered again
     # - ensure_communicating refuses to fetch y because the worker is paused
 
-    while "y" not in a.tasks or a.tasks["y"].state != "fetch":
+    while "y" not in a.state.tasks or a.state.tasks["y"].state != "fetch":
         await asyncio.sleep(0.01)
     await asyncio.sleep(0.1)
-    assert a.tasks["y"].state == "fetch"
+    assert a.state.tasks["y"].state == "fetch"
     assert "y" not in a.data
-    assert [ts.key for ts in a.data_needed] == ["y"]
+    assert [ts.key for ts in a.state.data_needed] == ["y"]
 
     # Unpausing kicks off ensure_communicating again
     a.status = Status.running
     assert await z == 3
-    assert a.tasks["y"].state == "memory"
+    assert a.state.tasks["y"].state == "memory"
     assert "y" in a.data
 
 
@@ -710,7 +710,7 @@ def leak():
         await future
     assert s.tasks["leak"].suspicious == 1
     assert not any(
-        (await c.run(lambda dask_worker: "leak" in dask_worker.tasks)).values()
+        (await c.run(lambda dask_worker: "leak" in dask_worker.state.tasks)).values()
     )
     future.release()
     while "leak" in s.tasks:
diff --git a/distributed/tests/test_worker_state_machine.py b/distributed/tests/test_worker_state_machine.py
--- a/distributed/tests/test_worker_state_machine.py
+++ b/distributed/tests/test_worker_state_machine.py
@@ -60,8 +60,8 @@ def test_TaskState_get_nbytes():
 
 def test_TaskState__to_dict():
     """Tasks that are listed as dependencies or dependents of other tasks are dumped as
-    a short repr and always appear in full directly under Worker.tasks. Uninteresting
-    fields are omitted.
+    a short repr and always appear in full directly under Worker.state.tasks.
+    Uninteresting fields are omitted.
     """
     x = TaskState("x", state="memory", done=True)
     y = TaskState("y", priority=(0,), dependencies={x})
@@ -380,7 +380,7 @@ async def test_fetch_to_compute(c, s, a, b):
     await f2
 
     assert_story(
-        b.log,
+        b.state.log,
         # FIXME: This log should be replaced with a StateMachineEvent log
         [
             (f2.key, "compute-task", "released"),
@@ -410,7 +410,7 @@ async def test_fetch_via_amm_to_compute(c, s, a, b):
     await f1
 
     assert_story(
-        b.log,
+        b.state.log,
         # FIXME: This log should be replaced with a StateMachineEvent log
         [
             (f1.key, "ensure-task-exists", "released"),
@@ -456,7 +456,7 @@ async def test_lose_replica_during_fetch(c, s, w1, w2, w3, as_deps):
             s.request_acquire_replicas(w1.address, ["x"], stimulus_id="test")
 
         await wait_for_state("x", "fetch", w1)
-        assert w1.tasks["x"].who_has == {w2.address, w3.address}
+        assert w1.state.tasks["x"].who_has == {w2.address, w3.address}
 
         assert len(s.tasks["x"].who_has) == 2
         await w2.close()
@@ -468,12 +468,12 @@ async def test_lose_replica_during_fetch(c, s, w1, w2, w3, as_deps):
         else:
             s.request_acquire_replicas(w1.address, ["x"], stimulus_id="test")
 
-        while w1.tasks["x"].who_has != {w3.address}:
+        while w1.state.tasks["x"].who_has != {w3.address}:
             await asyncio.sleep(0.01)
 
     await wait_for_state("x", "memory", w1)
     assert_story(
-        w1.story("request-dep"),
+        w1.state.story("request-dep"),
         [("request-dep", w3.address, {"x"})],
         # This tests that there has been no attempt to contact w2.
         # If the assumption being tested breaks, this will fail 50% of the times.
@@ -502,7 +502,7 @@ async def test_fetch_to_missing(c, s, a, b):
     # state will flip-flop between fetch and flight every 150ms, which is the retry
     # period for busy workers.
     await wait_for_state("x", "fetch", a)
-    assert b.address in a.busy_workers
+    assert b.address in a.state.busy_workers
 
     # Sever connection between b and s, but not between b and a.
     # If a tries fetching from b after this, b will keep responding {status: busy}.
@@ -512,7 +512,7 @@ async def test_fetch_to_missing(c, s, a, b):
     await wait_for_state("x", "missing", a)
 
     assert_story(
-        a.story("x"),
+        a.state.story("x"),
         [
             ("x", "ensure-task-exists", "released"),
             ("x", "released", "fetch", "fetch", {}),
@@ -554,7 +554,7 @@ async def test_new_replica_while_all_workers_in_flight(c, s, w1, w2):
         await wait([x, y])
         s.request_acquire_replicas(w1.address, ["x"], stimulus_id="test")
         await w3.in_get_data.wait()
-        assert w1.tasks["x"].state == "flight"
+        assert w1.state.tasks["x"].state == "flight"
         s.request_acquire_replicas(w1.address, ["y"], stimulus_id="test")
         # This cannot progress beyond fetch because w3 is already in flight
         await wait_for_state("y", "fetch", w1)
@@ -590,7 +590,7 @@ async def test_cancelled_while_in_flight(c, s, a, b):
     # Let the comm from b to a return the result
     event.set()
     # upon reception, x transitions cancelled->forgotten
-    while a.tasks:
+    while a.state.tasks:
         await asyncio.sleep(0.01)
 
 
@@ -642,7 +642,7 @@ async def test_forget_data_needed(c, s, a, b):
         await wait_for_state("x", "fetch", b)
         x.release()
         y.release()
-        while s.tasks or a.tasks or b.tasks:
+        while s.tasks or a.state.tasks or b.state.tasks:
             await asyncio.sleep(0.01)
 
     x = c.submit(inc, 2, key="x", workers=[a.address])
@@ -746,7 +746,7 @@ async def test_fetch_to_missing_on_refresh_who_has(c, s, w1, w2, w3):
     # (see Worker.retry_busy_worker_later)
     await wait_for_state("x", "fetch", w3)
     await wait_for_state("y", "fetch", w3)
-    assert w1.address in w3.busy_workers
+    assert w1.address in w3.state.busy_workers
     # w3 sent {op: request-refresh-who-has, keys: [x, y]}
     # There also may have been enough time for a refresh-who-has message to come back,
     # which reiterated what w3 already knew:
@@ -759,9 +759,9 @@ async def test_fetch_to_missing_on_refresh_who_has(c, s, w1, w2, w3):
     w3.handle_stimulus(
         RefreshWhoHasEvent(who_has={"x": [], "y": [w2.address]}, stimulus_id="test2")
     )
-    assert w3.tasks["x"].state == "missing"
-    assert w3.tasks["y"].state == "flight"
-    assert w3.tasks["y"].who_has == {w2.address}
+    assert w3.state.tasks["x"].state == "missing"
+    assert w3.state.tasks["y"].state == "flight"
+    assert w3.state.tasks["y"].who_has == {w2.address}
 
 
 @gen_cluster(client=True, nthreads=[("", 1)])
@@ -797,3 +797,26 @@ async def get_data(self, comm, *args, **kwargs):
 
         await wait_for_state("x", "missing", a)
         await wait_for_state("y", "missing", a)
+
+
+@gen_cluster()
+async def test_deprecated_worker_attributes(s, a, b):
+    n = a.state.comm_threshold_bytes
+    msg = (
+        "The `Worker.comm_threshold_bytes` attribute has been moved to "
+        "`Worker.state.comm_threshold_bytes`"
+    )
+    with pytest.warns(FutureWarning, match=msg):
+        assert a.comm_threshold_bytes == n
+    with pytest.warns(FutureWarning, match=msg):
+        a.comm_threshold_bytes += 1
+        assert a.comm_threshold_bytes == n + 1
+    assert a.state.comm_threshold_bytes == n + 1
+
+    # Old and new names differ
+    msg = (
+        "The `Worker.in_flight_tasks` attribute has been moved to "
+        "`Worker.state.in_flight_tasks_count`"
+    )
+    with pytest.warns(FutureWarning, match=msg):
+        assert a.in_flight_tasks == 0
diff --git a/distributed/utils_test.py b/distributed/utils_test.py
--- a/distributed/utils_test.py
+++ b/distributed/utils_test.py
@@ -1933,10 +1933,10 @@ class TaskStateMetadataPlugin(WorkerPlugin):
     """WorkPlugin to populate TaskState.metadata"""
 
     def setup(self, worker):
-        self.worker = worker
+        self.tasks = worker.state.tasks
 
     def transition(self, key, start, finish, **kwargs):
-        ts = self.worker.tasks[key]
+        ts = self.tasks[key]
 
         if start == "ready" and finish == "executing":
             ts.metadata["start_time"] = time()