Repository: sanic-org/sanic

Issue:
restart workers online (graceful restart) to hot reload, in production environment.
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Is your feature request related to a problem? Please describe.

Condition:  keep service available any time.  

# ---- client code----

```py
async def run():                                                                                        
    while 1:
        await post('http://127.0.0.1:8000/') 
```

when called app.m.restart("__ALL_PROCESSES__") in a worker, sanic crashed.

# ---- server code ----
```py
@app.post("/")
async def handler(request):
    app.m.restart('__ALL_PROCESSES__')
    return response.text('ok')


if __name__ == "__main__":
    app.run(debug=True, workers=2)
```

### Describe the solution you'd like

graceful restarting and reduce the effect when restarting.
my messy describe:
1. graceful restart workers; restart all workers will not crash,  if only 1 worker, block a little while (if worker not started yet) is ok.
2. a way to graceful restart worker one by one, code eg:
    woker_names = tuple(app.m.workers.keys())
    for woker_name in worker_names:
        ret_val = app.m.restart(worker_name)
        # here, the worker has been graceful restarted, ret_val is meaningful
3. may combine the above 2, when restarting all workers,  50% workers restarting, 50% old workers keep serving

### Additional context

simplify the api,

```py
app.m.restart('__ALL_PROCESSES__')   => app.m.restart_all()
```

thanks.


Discussion:

Resolving patch:
diff --git a/sanic/worker/multiplexer.py b/sanic/worker/multiplexer.py
--- a/sanic/worker/multiplexer.py
+++ b/sanic/worker/multiplexer.py
@@ -21,9 +21,14 @@ def ack(self):
             "state": ProcessState.ACKED.name,
         }
 
-    def restart(self, name: str = ""):
+    def restart(self, name: str = "", all_workers: bool = False):
+        if name and all_workers:
+            raise ValueError(
+                "Ambiguous restart with both a named process and"
+                " all_workers=True"
+            )
         if not name:
-            name = self.name
+            name = "__ALL_PROCESSES__:" if all_workers else self.name
         self._monitor_publisher.send(name)
 
     reload = restart  # no cov


Test patch:
diff --git a/tests/worker/test_multiplexer.py b/tests/worker/test_multiplexer.py
--- a/tests/worker/test_multiplexer.py
+++ b/tests/worker/test_multiplexer.py
@@ -1,6 +1,6 @@
 from multiprocessing import Event
 from os import environ, getpid
-from typing import Any, Dict
+from typing import Any, Dict, Type, Union
 from unittest.mock import Mock
 
 import pytest
@@ -117,3 +117,26 @@ def test_properties(
     assert m.workers == worker_state
     assert m.state == worker_state["Test"]
     assert isinstance(m.state, WorkerState)
+
+
+@pytest.mark.parametrize(
+    "params,expected",
+    (
+        ({}, "Test"),
+        ({"name": "foo"}, "foo"),
+        ({"all_workers": True}, "__ALL_PROCESSES__:"),
+        ({"name": "foo", "all_workers": True}, ValueError),
+    ),
+)
+def test_restart_params(
+    monitor_publisher: Mock,
+    m: WorkerMultiplexer,
+    params: Dict[str, Any],
+    expected: Union[str, Type[Exception]],
+):
+    if isinstance(expected, str):
+        m.restart(**params)
+        monitor_publisher.send.assert_called_once_with(expected)
+    else:
+        with pytest.raises(expected):
+            m.restart(**params)