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:
codecov[bot]: # [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2622?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Base: **87.825**% // Head: **87.792**% // Decreases project coverage by **`-0.032%`** :warning:
> Coverage data is based on head [(`d6097be`)](https://codecov.io/gh/sanic-org/sanic/pull/2622?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) compared to base [(`d404116`)](https://codecov.io/gh/sanic-org/sanic/commit/d4041161c79f949766102ebc2023c136b8f2cb73?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).
> Patch coverage: 50.000% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2622       +/-   ##
=============================================
- Coverage   87.825%   87.792%   -0.033%     
=============================================
  Files           71        71               
  Lines         5380      5382        +2     
  Branches       897       898        +1     
=============================================
  Hits          4725      4725               
- Misses         474       475        +1     
- Partials       181       182        +1     
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2622?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/worker/multiplexer.py](https://codecov.io/gh/sanic-org/sanic/pull/2622/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvd29ya2VyL211bHRpcGxleGVyLnB5) | `93.750% <50.000%> (-6.250%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2622?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/sanic/worker/multiplexer.py b/sanic/worker/multiplexer.py
index f92e7e08..91030fe6 100644
--- a/sanic/worker/multiplexer.py
+++ b/sanic/worker/multiplexer.py
@@ -21,9 +21,14 @@ class WorkerMultiplexer:
             "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
index 338d3294..5047ef77 100644
--- 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)