Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File upload can fail with gunicorn sync workers (Windows File Explorer) #332

Open
bbx0 opened this issue Dec 11, 2024 · 4 comments
Open
Labels

Comments

@bbx0
Copy link

bbx0 commented Dec 11, 2024

Describe the bug
When wsgidav is run with gunicorn and more than 1 sync worker, the file upload can fail and a 0 byte file is produced.

To Reproduce
Steps to reproduce the behavior:

  1. Start wsgidav with gunicron workers.
# syntax=docker.io/docker/dockerfile:1
FROM docker.io/python:3-alpine
RUN pip install --no-cache-dir wsgidav lxml gunicorn

# Set workers: 1 to avoid the issue
COPY <<'EOF' /etc/wsgidav/wsgidav.yaml
host: 0.0.0.0
port: 8080
verbose: 6

server: gunicorn
server_args:
  workers: 5
  threads: 1
  timeout: 1200

provider_mapping:
  '/seafdav': '/var/wsgidav-root'
EOF

VOLUME ["/var/wsgidav-root"]
EXPOSE 8080
STOPSIGNAL SIGINT
CMD ["wsgidav", "--config=/etc/wsgidav/wsgidav.yaml", "--auth=anonymous"]
  1. Confirm gunicorn uses the sync worker_class: [INFO] Using worker: sync
  2. Open the Windows File Explorer and add a Network Address http://127.0.0.1:8080/seafdav
  3. Copy a single file into the Network Share
  4. Repeat step 4 until the issue occurs. Press F5 (refresh) in case the issue is not trigger at once.

Expected behavior
File gets uploaded

Screenshots, Log-Files, Stacktrace

Debug Log
Configuration(/etc/wsgidav/wsgidav.yaml):
{'_config_file': '/etc/wsgidav/wsgidav.yaml',
 '_config_root': '/etc/wsgidav',
 'add_header_MS_Author_Via': True,
 'dir_browser': {'davmount': True,
                 'davmount_links': False,
                 'directory_slash': True,
                 'enable': True,
                 'htdocs_path': None,
                 'icon': True,
                 'ignore': ['.DS_Store', '._*', 'Thumbs.db'],
                 'libre_office_support': True,
                 'ms_sharepoint_support': True,
                 'response_trailer': True,
                 'show_user': True},
 'fs_dav_provider': {'follow_symlinks': False, 'shadow_map': {}},
 'host': '0.0.0.0',
 'hotfixes': {'emulate_win32_lastmod': False,
              're_encode_path_info': True,
              'unquote_path_info': False},
 'http_authenticator': {'accept_basic': True,
                        'accept_digest': True,
                        'default_to_digest': True,
                        'domain_controller': None,
                        'trusted_auth_header': None},
 'lock_storage': True,
 'logging': {'debug_methods': [],
             'enable': None,
             'enable_loggers': [],
             'logger_date_format': '%H:%M:%S',
             'logger_format': '%(asctime)s.%(msecs)03d - %(levelname)-8s: '
                              '%(message)s'},
 'middleware_stack': [<class 'wsgidav.mw.cors.Cors'>,
                      <class 'wsgidav.error_printer.ErrorPrinter'>,
                      <class 'wsgidav.http_authenticator.HTTPAuthenticator'>,
                      <class 'wsgidav.dir_browser._dir_browser.WsgiDavDirBrowser'>,
                      <class 'wsgidav.request_resolver.RequestResolver'>],
 'mount_path': None,
 'mutable_live_props': [],
 'port': 8080,
 'property_manager': None,
 'provider_mapping': {'/seafdav': '/var/wsgidav-root'},
 'server': 'gunicorn',
 'server_args': {'threads': 1, 'timeout': 1200, 'workers': 5},
 'simple_dc': {'user_mapping': {}},
 'suppress_version_info': False,
 'verbose': 6}
12:09:58.893 - WARNING : App wsgidav.mw.cors.Cors(None).is_disabled() returned True: skipping.
12:09:58.895 - INFO    : WsgiDAV/4.3.3 Python/3.13.1 Linux-6.12.4-arch1-1-x86_64-with
12:09:58.895 - INFO    : Default encoding: 'utf-8' (file system: 'utf-8')
12:09:58.895 - INFO    : Lock manager:      LockManager(LockStorageDict)
12:09:58.895 - INFO    : Property manager:  None
12:09:58.895 - INFO    : Domain controller: SimpleDomainController()
12:09:58.895 - INFO    : Middleware stack:
12:09:58.895 - INFO    :   - wsgidav.error_printer.ErrorPrinter
12:09:58.895 - INFO    :   - wsgidav.http_authenticator.HTTPAuthenticator
12:09:58.895 - INFO    :   - wsgidav.dir_browser._dir_browser.WsgiDavDirBrowser
12:09:58.895 - INFO    :   - wsgidav.request_resolver.RequestResolver
12:09:58.895 - INFO    : Registered DAV providers by route:
12:09:58.895 - INFO    :   - '/:dir_browser': FilesystemProvider for path '/usr/local/lib/python3.13/site-packages/wsgidav/dir_browser/htdocs' (Read-Only) (anonymous)
12:09:58.895 - INFO    :   - '/seafdav': FilesystemProvider for path '/var/wsgidav-root' (Read-Write) (anonymous)
12:09:58.895 - WARNING : Basic authentication is enabled: It is highly recommended to enable SSL.
12:09:58.895 - WARNING : Share '/seafdav' will allow anonymous write access.
12:09:58.895 - WARNING : Share '/:dir_browser' will allow anonymous write access.
12:09:58.912 - INFO    : Running WsgiDAV/4.3.3 gunicorn/23.0.0 Python/3.13.1 ...
[2024-12-11 12:09:58 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2024-12-11 12:09:58 +0000] [1] [INFO] Listening at: http://0.0.0.0:8080 (1)
[2024-12-11 12:09:58 +0000] [1] [INFO] Using worker: sync
[2024-12-11 12:09:58 +0000] [3] [INFO] Booting worker with pid: 3
[2024-12-11 12:09:59 +0000] [4] [INFO] Booting worker with pid: 4
[2024-12-11 12:09:59 +0000] [5] [INFO] Booting worker with pid: 5
[2024-12-11 12:09:59 +0000] [6] [INFO] Booting worker with pid: 6
[2024-12-11 12:09:59 +0000] [7] [INFO] Booting worker with pid: 7
12:10:09.186 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PROPFIND " length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 207 Multi-Status
12:10:09.189 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PROPFIND " length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 207 Multi-Status
12:10:09.395 - DEBUG   : Raising DAVError 404 Not Found: /test1.txt
12:10:09.395 - DEBUG   : Caught (404, '/test1.txt')
12:10:09.395 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PROPFIND /test1.txt" length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 404 Not Found
12:10:09.398 - DEBUG   : check_write_permission(/seafdav/, 0, [], )
12:10:09.398 - DEBUG   :   checking /seafdav/
12:10:09.398 - DEBUG   :   checking /
12:10:09.398 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PUT /test1.txt" length=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 201 Created
12:10:09.401 - DEBUG   : checkLockPermission(/seafdav/test1.txt, exclusive, infinity, )
12:10:09.401 - DEBUG   : LockStorageDict.set('/seafdav/test1.txt'): Lock(<3571..>, '/seafdav/test1.txt', , exclusive, depth-infinity, until 2024-12-11 13:10:09 (in 3599.9999821186066 seconds)
12:10:09.401 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "LOCK /test1.txt" length=205, depth=infinity, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 200 OK
12:10:09.405 - DEBUG   : parse_if_header_dict
{'*': [[(True,
         'locktoken',
         'opaquelocktoken:0x3571d9428cbc168c6be7068c6125564d8ce1a355c5217a7c058fda774dbb40e6')]]}
12:10:09.405 - DEBUG   : test_if_header_dict(/seafdav/test1.txt, [], 16784429-1733919009-0)
12:10:09.405 - DEBUG   :   -> FAILED
12:10:09.405 - DEBUG   : Raising DAVError 412 Precondition Failed: 'If' header condition failed.
12:10:09.405 - DEBUG   : Caught (412, "'If' header condition failed.")
12:10:09.405 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PROPPATCH /test1.txt" length=443, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 412 Precondition Failed
12:10:09.409 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "HEAD /test1.txt" depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 200 OK
12:10:09.412 - DEBUG   : parse_if_header_dict
{'*': [[(True,
         'locktoken',
         'opaquelocktoken:0x3571d9428cbc168c6be7068c6125564d8ce1a355c5217a7c058fda774dbb40e6')]]}
12:10:09.412 - DEBUG   : test_if_header_dict(/seafdav/test1.txt, [], 16784429-1733919009-0)
12:10:09.412 - DEBUG   :   -> FAILED
12:10:09.412 - DEBUG   : Raising DAVError 412 Precondition Failed: 'If' header condition failed.
12:10:09.412 - DEBUG   : Caught (412, "'If' header condition failed.")
12:10:09.413 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PUT /test1.txt" length=4, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 412 Precondition Failed
12:10:09.415 - DEBUG   : parse_if_header_dict
{'*': [[(True,
         'locktoken',
         'opaquelocktoken:0x3571d9428cbc168c6be7068c6125564d8ce1a355c5217a7c058fda774dbb40e6')]]}
12:10:09.416 - DEBUG   : test_if_header_dict(/seafdav/test1.txt, [], 16784429-1733919009-0)
12:10:09.416 - DEBUG   :   -> FAILED
12:10:09.416 - DEBUG   : Raising DAVError 412 Precondition Failed: 'If' header condition failed.
12:10:09.416 - DEBUG   : Caught (412, "'If' header condition failed.")
12:10:09.416 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "DELETE /test1.txt" depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 412 Precondition Failed
12:10:09.422 - DEBUG   : check_write_permission(/seafdav/test1.txt, 0, [], )
12:10:09.422 - DEBUG   :   checking /seafdav/test1.txt
12:10:09.422 - DEBUG   :   checking /seafdav/
12:10:09.422 - DEBUG   :   checking /
12:10:09.423 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PUT /test1.txt" length=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 204 No Content
12:10:09.425 - DEBUG   : checkLockPermission(/seafdav/test1.txt, exclusive, infinity, )
12:10:09.425 - DEBUG   : LockStorageDict.set('/seafdav/test1.txt'): Lock(<8895..>, '/seafdav/test1.txt', , exclusive, depth-infinity, until 2024-12-11 13:10:09 (in 3599.999984264374 seconds)
12:10:09.426 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "LOCK /test1.txt" length=205, depth=infinity, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 200 OK
12:10:09.430 - DEBUG   : parse_if_header_dict
{'*': [[(True,
         'locktoken',
         'opaquelocktoken:0x8895a787e59bb8eda7bcd0483bf954d4756276c146c5cf8d3e1f3747be3fb679')]]}
12:10:09.430 - DEBUG   : test_if_header_dict(/seafdav/test1.txt, ['opaquelocktoken:0x8895a787e59bb8eda7bcd0483bf954d4756276c146c5cf8d3e1f3747be3fb679'], 16784429-1733919009-0)
12:10:09.430 - DEBUG   : check_write_permission(/seafdav/test1.txt, 0, ['opaquelocktoken:0x8895a787e59bb8eda7bcd0483bf954d4756276c146c5cf8d3e1f3747be3fb679'], )
12:10:09.430 - DEBUG   :   checking /seafdav/test1.txt
12:10:09.430 - DEBUG   :      lock=Lock(<8895..>, '/seafdav/test1.txt', , exclusive, depth-infinity, until 2024-12-11 13:10:09 (in 3599.9952142238617 seconds)
12:10:09.430 - DEBUG   :   checking /seafdav/
12:10:09.430 - DEBUG   :   checking /
12:10:09.430 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PROPPATCH /test1.txt" length=443, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 207 Multi-Status
12:10:09.433 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "HEAD /test1.txt" depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 200 OK
12:10:09.435 - DEBUG   : parse_if_header_dict
{'*': [[(True,
         'locktoken',
         'opaquelocktoken:0x8895a787e59bb8eda7bcd0483bf954d4756276c146c5cf8d3e1f3747be3fb679')]]}
12:10:09.435 - DEBUG   : test_if_header_dict(/seafdav/test1.txt, [], 16784429-1733919009-0)
12:10:09.436 - DEBUG   :   -> FAILED
12:10:09.436 - DEBUG   : Raising DAVError 412 Precondition Failed: 'If' header condition failed.
12:10:09.436 - DEBUG   : Caught (412, "'If' header condition failed.")
12:10:09.436 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "PUT /test1.txt" length=4, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 412 Precondition Failed
12:10:09.438 - DEBUG   : parse_if_header_dict
{'*': [[(True,
         'locktoken',
         'opaquelocktoken:0x8895a787e59bb8eda7bcd0483bf954d4756276c146c5cf8d3e1f3747be3fb679')]]}
12:10:09.438 - DEBUG   : test_if_header_dict(/seafdav/test1.txt, [], 16784429-1733919009-0)
12:10:09.438 - DEBUG   :   -> FAILED
12:10:09.439 - DEBUG   : Raising DAVError 412 Precondition Failed: 'If' header condition failed.
12:10:09.439 - DEBUG   : Caught (412, "'If' header condition failed.")
12:10:09.439 - INFO    : 10.89.2.38 - (anonymous) - [2024-12-11 12:10:09] "DELETE /test1.txt" depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 412 Precondition Failed

Screenshot

Environment:

WsgiDAV/4.3.3 Python/3.13.1(64 bit) Linux-6.12.4-arch1-1-x86_64-with
Python from: /usr/local/bin/python3.13

gunicorn/23.0.0

Which WSGI server was used (cheroot, ext-wsgiutils, gevent, gunicorn, paste, uvicorn, wsgiref, ...)?

  • gunicorn with 5 sync workers

Which WebDAV client was used (MS File Explorer, MS Office, macOS Finder, WinSCP, Windows, file mapping, ...)?

  • MS File Explorer (Windows 10)

Additional context
#324 #2809

The sync worker is the default for gunicorn, as far as I understand. The issue seems not occur when gthread is used instead e.g., by setting workers: 1 threads: 5. (Then no test_if_header_dict() -> FAILED messages are not shown, and the file upload works.)

@bbx0 bbx0 added the bug label Dec 11, 2024
@mar10
Copy link
Owner

mar10 commented Dec 12, 2024

Without having investigated in depth:
gunicorn seems to start different processes in this mode, and LockManager(LockStorageDict) works in-memory, which is a problem, I guess.
Have you tried without locking or other LockManagers?

@bbx0
Copy link
Author

bbx0 commented Dec 12, 2024

Thank you for picking up on this!

With lock_storage: null it seems just no PROPPATCH is attempted after the LOCK failed (also causing a 0 byte file).

[wsgidav] | 21:12:18.084 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:18] "PROPFIND /test1.txt" length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 404 Not Found
[wsgidav] | 21:12:18.088 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:18] "PUT /test1.txt" length=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 201 Created
[wsgidav] | 21:12:18.090 - ERROR   : Invalid HTTP method {requestmethod!r}
[wsgidav] | 21:12:18.090 - DEBUG   : Raising DAVError 405 Method Not Allowed
[wsgidav] | 21:12:18.090 - DEBUG   : Caught (405, None)
[wsgidav] | 21:12:18.090 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:18] "LOCK /test1.txt" length=205, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 405 Method Not Allowed
[wsgidav] | 21:12:18.093 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:18] "PROPFIND /test1.txt" length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.001sec -> 207 Multi-Status
[wsgidav] | 21:12:18.096 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:18] "PROPFIND /test1.txt" length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 207 Multi-Status
[wsgidav] | 21:12:18.099 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:18] "PROPFIND /test1.txt" length=0, depth=0, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 207 Multi-Status
[wsgidav] | 21:12:22.583 - INFO    : 10.89.2.2 - (anonymous) - [2024-12-12 21:12:22] "PROPFIND " length=0, depth=1, connection="Keep-Alive", agent="Microsoft-WebDAV-MiniRedir/10.0.19045", elap=0.000sec -> 207 Multi-Status

With LockStorageShelve I actually can't reproduce it at the moment. 😀 Though, I played with the LockManager options yesterday and I believe I saw it happen. That said, today LockStorageShelve seems just to work fine. (It's very likely I just did something wrong with testing yesterday.)

lock_storage:
  class: wsgidav.lock_man.lock_storage.LockStorageShelve
  kwargs:
    storage_path: /var/lib/wsgidav/locks.shelve`
Dockerfile
# syntax=docker.io/docker/dockerfile:1
FROM docker.io/python:3-alpine
RUN pip install --no-cache-dir wsgidav lxml gunicorn

# Set workers: 1 to avoid the issue
COPY <<'EOF' /etc/wsgidav/wsgidav.yaml
host: 0.0.0.0
port: 8080
verbose: 6

server: gunicorn
server_args:
  workers: 5
  threads: 1
  timeout: 1200

provider_mapping:
  '/seafdav': '/srv/wsgidav'

#lock_storage: null
#lock_storage: true
lock_storage:
  class: wsgidav.lock_man.lock_storage.LockStorageShelve
  kwargs:
    storage_path: /var/lib/wsgidav/locks.shelve
EOF

VOLUME ["/srv/wsgidav", "/var/lib/wsgidav"]
EXPOSE 8080
STOPSIGNAL SIGINT
CMD ["wsgidav", "--config=/etc/wsgidav/wsgidav.yaml", "--auth=anonymous"]

@mar10
Copy link
Owner

mar10 commented Dec 13, 2024

The shelve module is not safe for concurrent access either.

If concurrency is the root problem, the redis lock storage may be a better choice (See also #186)

@bbx0
Copy link
Author

bbx0 commented Dec 13, 2024

Oh, good catch!

I gave LockStorageRedis a try and it seems to be fine. I cannot reproduce the issue with it.

docker-compose.yaml
name: seafdav
configs:
  wsgidav.yaml:
    content: |
      host: 0.0.0.0
      port: 8080
      verbose: 6
      server: gunicorn
      server_args:
        workers: 5
        threads: 1
        timeout: 1200
      provider_mapping:
        '/seafdav': '/srv/wsgidav'
      lock_storage:
        class: wsgidav.lock_man.lock_storage_redis.LockStorageRedis
        kwargs:
          host: redis
          port: 6379
services:
  wsgidav:
    depends_on: 
      - redis
    build:
      dockerfile_inline: |
        FROM docker.io/python:3-alpine
        RUN pip install --no-cache-dir wsgidav lxml gunicorn redis
        VOLUME ["/srv/wsgidav", "/var/lib/wsgidav"]
        EXPOSE 8080
        STOPSIGNAL SIGINT
        CMD ["wsgidav", "--config=/etc/wsgidav/wsgidav.yaml", "--auth=anonymous"]
    configs:
      - source: wsgidav.yaml
        target: /etc/wsgidav/wsgidav.yaml
    ports:
      - "8080:8080"
    volumes:
      - ./wsgidav_data:/srv/wsgidav
    networks:
    - seafdav
  redis:
    image: docker.io/redis:7-alpine
    networks:
    - seafdav
networks:
  seafdav:
      name: seafdav

If concurrency is the actual cause, there is not much we can do here besides documentation, I guess. We can either

  • use gunicorn with 1 worker or
  • use LockStorageRedis in combination with a Redis instance.

Maybe wsgidav could issue an error (or a warning), when started with a known bad set of parameters for gunicorn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants