Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion webknossos/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
### Changed

### Fixed

- faster deletion of S3 directories by using the delete methods from the s3fs file system, retry more S3 errors observed when deleting directories [#4554](https://github.com/scalableminds/webknossos-libs/pull/4554)

## [3.4.2](https://github.com/scalableminds/webknossos-libs/releases/tag/v3.4.2) - 2026-04-28
[Commits](https://github.com/scalableminds/webknossos-libs/compare/v3.4.1...v3.4.2)
Expand Down
39 changes: 21 additions & 18 deletions webknossos/webknossos/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,24 +322,13 @@ def strip_trailing_slash(path: UPath) -> UPath:


def rmtree(path: UPath) -> None:
def _walk(path: UPath) -> Iterator[UPath]:
if path.exists():
if path.is_dir() and not path.is_symlink():
for p in path.iterdir():
yield from _walk(p)
yield path

for sub_path in _walk(path):
try:
if sub_path.is_file() or sub_path.is_symlink():
sub_path.unlink()
elif sub_path.is_dir():
sub_path.rmdir()
except FileNotFoundError: # noqa: PERF203 `try`-`except` within a loop incurs performance overhead
# Some implementations `UPath` do not have explicit directory representations
# Therefore, directories only exist, if they have files. Consequently, when
# all files have been deleted, the directory does not exist anymore.
pass
try:
path.fs.delete(str(path), recursive=True)
except FileNotFoundError:
# Some implementations of `UPath` do not have explicit directory representations
# Therefore, directories only exist, if they have files. Consequently, when
# all files have been deleted, the directory does not exist anymore.
pass


def copytree(
Expand Down Expand Up @@ -535,6 +524,8 @@ def set_s3fs_retry_settings(
*, read_timeout: int = 60, connect_timeout: int = 30, retries: int = 10
) -> None:
import s3fs
from aiohttp.client_exceptions import ClientPayloadError
from aiohttp.http_exceptions import TransferEncodingError
from botocore.exceptions import ClientError, ConnectionClosedError

s3fs_logger = logging.getLogger("s3fs")
Expand Down Expand Up @@ -563,10 +554,22 @@ def custom_s3fs_error_handler(exception: Exception) -> bool:
stack_info=True,
)
return True
if (
"connection was closed" in str(exception).lower()
or "not enough data for satisfy" in str(exception).lower()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is likely a typo in the error string comparison. The common error message from underlying libraries like urllib3 or http.client is "not enough data to satisfy", not "for satisfy". Using the correct string will ensure the retry logic triggers as intended for these transient network errors.

Suggested change
or "not enough data for satisfy" in str(exception).lower()
or "not enough data to satisfy" in str(exception).lower()

Copy link
Copy Markdown
Contributor Author

@Tobias314 Tobias314 Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, the error message that we have seen reads "Not enough data for satisfy transfer length header"

):
s3fs_logger.warning(
f"Retrying unexpected error: {exception}",
exc_info=exception,
stack_info=True,
)
return True

return False

s3fs.add_retryable_error(ConnectionClosedError)
s3fs.add_retryable_error(TransferEncodingError)
s3fs.add_retryable_error(ClientPayloadError)
s3fs.set_custom_error_handler(custom_s3fs_error_handler)


Expand Down
Loading