Repository: encode/uvicorn

Issue:
Uvicorn fails to start in 0.10.1+ (Can't pickle local object)
We're using uvicorn for a project and I recently discovered we could no longer start the web server in our usual testing environment. I'm deploying into docker on Ubuntu, and am calling uvicorn with the following code snippet:
```python
from project import app
workers = 1 if platform == 'win32' or debug is True else 6
uvicorn.run(app, host='0.0.0.0', port=config.web_port, debug=debug, workers=workers)
```
And I get the following error (full traceback below):
`AttributeError: Can't pickle local object 'request_response.<locals>.app'`

I ran some tests, and the service starts successfully with version 0.10.0 and older of uvicorn, but crashes in the same way with 0.10.1 and higher.
0.9.1: ok
0.10.0: ok
0.10.1: crash
0.10.2: crash
0.10.3: crash

Taking a look, I see commit 2e88dcc having something to do with the run/multiprocessing pieces and this commit occurred between 0.10.0 and 0.10.1 so that might be a good starting point, although I can't see where the issue is reading through myself.
I also noted an old issue with a seemingly related error message that specifically had to do with use of the `reload` parameter (which we're not using here) although it might be helpful as a reference:
https://github.com/encode/uvicorn/issues/315

```
2019-11-08T00:35:33.437764657Z MIGRATION_PATH not set in environment
2019-11-08T00:35:33.438091721Z setup_logging() is setting up logging..
2019-11-08T00:35:33.456107924Z           migrate.py [I] Running any pending migrations
2019-11-08T00:35:33.460162581Z           migrate.py [I] Migration process ended; 403 -> 403.
2019-11-08T00:35:33.466950665Z            models.py [W] email-validator not installed, email fields will be treated as str.
2019-11-08T00:35:33.466969944Z To install, run: pip install email-validator
2019-11-08T00:35:33.830898661Z            _views.py [W] MAPFRONT_WEBSOCKET_PORT not set in environment; using web port

2019-11-08T00:35:33.831350874Z            _views.py [I] Failed to mount static files directory, likely in a production environment. Skipping mount.
2019-11-08T00:35:33.866255362Z INFO:     Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
2019-11-08T00:35:33.866311831Z INFO:     Started parent process [1]
2019-11-08T00:35:33.867962280Z Using "/etc/combox/config.json" config file
2019-11-08T00:35:33.871418418Z Traceback (most recent call last):
2019-11-08T00:35:33.871435150Z   File "/usr/local/bin/combox_mapfront_web", line 10, in <module>
2019-11-08T00:35:33.871440161Z     sys.exit(main())
2019-11-08T00:35:33.871443299Z   File "/usr/local/lib/python3.6/site-packages/combox/mapfront/web/service.py", line 40, in main
2019-11-08T00:35:33.871446466Z     uvicorn.run(app, host='0.0.0.0', port=config.web_port, debug=debug, workers=workers)
2019-11-08T00:35:33.871449524Z   File "/usr/local/lib/python3.6/site-packages/uvicorn/main.py", line 308, in run

2019-11-08T00:35:33.871452700Z     supervisor.run(server.run, sockets=[sock])
2019-11-08T00:35:33.871470157Z   File "/usr/local/lib/python3.6/site-packages/uvicorn/supervisors/multiprocess.py", line 59, in run
2019-11-08T00:35:33.871473728Z     process.start()
2019-11-08T00:35:33.871476617Z   File "/usr/local/lib/python3.6/multiprocessing/process.py", line 105, in start
2019-11-08T00:35:33.871479708Z     self._popen = self._Popen(self)
2019-11-08T00:35:33.871482617Z   File "/usr/local/lib/python3.6/multiprocessing/context.py", line 284, in _Popen
2019-11-08T00:35:33.871485704Z     return Popen(process_obj)
2019-11-08T00:35:33.871488577Z   File "/usr/local/lib/python3.6/multiprocessing/popen_spawn_posix.py", line 32, in __init__
2019-11-08T00:35:33.871491654Z     super().__init__(process_obj)
2019-11-08T00:35:33.871494577Z   File "/usr/local/lib/python3.6/multiprocessing/popen_fork.py", line 19, in __init__

2019-11-08T00:35:33.871497669Z     self._launch(process_obj)
2019-11-08T00:35:33.871500499Z   File "/usr/local/lib/python3.6/multiprocessing/popen_spawn_posix.py", line 47, in _launch
2019-11-08T00:35:33.871503560Z     reduction.dump(process_obj, fp)
2019-11-08T00:35:33.871506459Z   File "/usr/local/lib/python3.6/multiprocessing/reduction.py", line 60, in dump
2019-11-08T00:35:33.871509550Z     ForkingPickler(file, protocol).dump(obj)
2019-11-08T00:35:33.871512846Z AttributeError: Can't pickle local object 'request_response.<locals>.app'

```


Discussion:
Thanks for raising this!

Rather than importing the project, you'll need to use the string format for the app argument instead...

```python
workers = 1 if platform == 'win32' or debug is True else 6
uvicorn.run('project:app', host='0.0.0.0', port=config.web_port, debug=debug, workers=workers)
```

We'd already deprecated [the ability to use `reload=True` when passing an application instance](https://github.com/encode/uvicorn/blob/f846a3514d5c152f300f03b882c05aaf915e73cb/uvicorn/main.py#L299), in favour of forcing the string style. It looks to me like we'll want to do the same with `workers=` too.

The issue is to do with how Python reloads the application inside new processes. If you're passing the application instance then it attempts to pickle it in order to send it to the new process, but if you're passing an import string, it can cleanly reload it each time.

This is complicated by differing behaviours depending on the type of multiprocessing being used... ("spawn" or "fork") https://docs.python.org/3.8/library/multiprocessing.html

The commit you've seen switched the default to "spawn" everywhere. Previously we were just using whatever the platform default was.

This means that prior to 0.10.1 your example code would have worked fine on some platforms (Unix, MacOS under Python 3.6/3.7) and failed on others (Windows, MacOS on Python 3.8).
From 0.10.2 your example code at least fails reliably under all platforms. 😃

Outcome: We need to document that `workers=...` requires the import-string-style only, and raise an explicit, helpful error when an application instance is passed.

Also while we're here, you may as well change `debug=True` to `reload=True`.

https://github.com/encode/uvicorn/issues/480

Resolving patch:
diff --git a/uvicorn/main.py b/uvicorn/main.py
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -294,9 +294,11 @@ def run(app, **kwargs):
     config = Config(app, **kwargs)
     server = Server(config=config)
 
-    if config.reload and not isinstance(app, str):
+    if (config.reload or config.workers > 1) and not isinstance(app, str):
         logger = logging.getLogger("uvicorn.error")
-        logger.warn("auto-reload only works when app is passed as an import string.")
+        logger.warn(
+            "You must pass the application as an import string to enable 'reload' or 'workers'."
+        )
 
     if config.should_reload:
         sock = config.bind_socket()


Test patch: