Repository: psf/black

Issue:
Black Freezes in Containers on Nodes with a Large `os.cpu_count()`
**Describe the bug**

Black attempts to run with too many executed threads when running in a container (e.g. in kubernetes). 

It is well understood that containers **do not** mask the number of physical cores available, despite the fact that the container itself might be limited by the amount of CPU it can use at one time. This means that using the result of `os.cpu_count()` to spin up that many threads can cause a huge number of threads to be created. The memory overhead of this can cause the program to freeze or crash. See how this has impacted other projects:

* https://bugs.python.org/issue36054
* https://github.com/benoitc/gunicorn/issues/2028
* https://medium.com/@chipiga86/python-kubernetes-cpu-pinning-eeaeee977c2e
* https://stackoverflow.com/questions/47989418/multiprocessing-python-program-inside-docker

We use black in our CI (via precommit) stack which runs on Kubernetes and are currently unable to run any builds using black (and so have had to turn it off for now).

**To Reproduce**

1. Simply log into your nearest 1000 core machine
2. Start a container on the machine with reasonable resource limits (e.g. 2 CPU & 1GB RAM)
3. Run `black file1.py file2.py` (must be more than 1 file)
4. Watch as black grinds to a halt trying to start 1000 threads

**Expected behavior**

Black does not grind to a halt...

**Environment:**

- Version: 21.9b0 
- OS and Python version: Linux/[3.7|3.8|3.9]

**Does this bug also happen on main?**

Yes, you can see the offending code on HEAD [here](https://github.com/psf/black/blob/main/src/black/__init__.py#L652).

**Resolution**

I would suggest the easiest solution would be to add a `--workers` argument to `black` to optionally set the number of threads. I have a PR ready to go. 


Discussion:
We try to limit the number of CLI switches. Are there generic APIs black could possibly query to:
- Workout if we're in a container
  - If so, maybe generate to 1 vCPU?
- Workout how many CPUs a container has?

that work in the majority of Linux containers?

Just asking and playing devils advocate here to see if we could do saner auto detection rather than add yet another a CLI switch (and I'm not ruling it out just wondering here).

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -95,6 +95,8 @@ def from_configuration(
 # Legacy name, left for integrations.
 FileMode = Mode
 
+DEFAULT_WORKERS = os.cpu_count()
+
 
 def read_pyproject_toml(
     ctx: click.Context, param: click.Parameter, value: Optional[str]
@@ -318,6 +320,14 @@ def validate_regex(
         "editors that rely on using stdin."
     ),
 )
+@click.option(
+    "-W",
+    "--workers",
+    type=click.IntRange(min=1),
+    default=DEFAULT_WORKERS,
+    show_default=True,
+    help="Number of parallel workers",
+)
 @click.option(
     "-q",
     "--quiet",
@@ -383,6 +393,7 @@ def main(
     extend_exclude: Optional[Pattern],
     force_exclude: Optional[Pattern],
     stdin_filename: Optional[str],
+    workers: int,
     src: Tuple[str, ...],
     config: Optional[str],
 ) -> None:
@@ -468,6 +479,7 @@ def main(
                 write_back=write_back,
                 mode=mode,
                 report=report,
+                workers=workers,
             )
 
     if verbose or not quiet:
@@ -644,12 +656,17 @@ def reformat_one(
 
 
 def reformat_many(
-    sources: Set[Path], fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
+    sources: Set[Path],
+    fast: bool,
+    write_back: WriteBack,
+    mode: Mode,
+    report: "Report",
+    workers: Optional[int],
 ) -> None:
     """Reformat multiple files using a ProcessPoolExecutor."""
     executor: Executor
     loop = asyncio.get_event_loop()
-    worker_count = os.cpu_count()
+    worker_count = workers if workers is not None else DEFAULT_WORKERS
     if sys.platform == "win32":
         # Work around https://bugs.python.org/issue26903
         worker_count = min(worker_count, 60)


Test patch: