Backblaze / b2-sdk-python

Python library to access B2 cloud storage.
Other
186 stars 61 forks source link

"Exception ignored" message emitted to console at end of I/O operation #524

Closed rcook closed 2 weeks ago

rcook commented 3 weeks ago

I think this is benign since the file is uploaded in its entirety, but it does messed up the output of my script:

Exception ignored in: <b2sdk._internal.stream.progress.ReadingStreamWithProgress object at 0x00000256ED631870>
Traceback (most recent call last):
  File "C:\python\Lib\site-packages\b2sdk\_internal\stream\wrapper.py", line 55, in flush
    self.stream.flush()
  File "C:\python\Lib\site-packages\b2sdk\_internal\stream\wrapper.py", line 55, in flush
    self.stream.flush()
ValueError: I/O operation on closed file.

This is the offending line of code: https://github.com/Backblaze/b2-sdk-python/blob/master/b2sdk/_internal/stream/wrapper.py#L55

Perhaps it would be as simple as wrapping the call to self.stream.flush() and catching ValueError.

ppolewicz commented 3 weeks ago

Does this happen every time? I've never seen it so far.

Does your script do something weird with descriptors?

rcook commented 3 weeks ago

@ppolewicz : It does not happen every single time, but it happens often enough that it's distracting. Here's the class from my code that calls into the b2sdk:

from b2sdk.v2 import \
    AbstractAction, \
    AbstractFileSyncPolicy, \
    B2Api, \
    DownAndDeletePolicy, \
    LocalDeleteAction, \
    SqliteAccountInfo, \
    SyncPolicyManager
from b2sdk.v2 import ScanPoliciesManager
from b2sdk.v2 import parse_folder
from b2sdk.v2 import Synchronizer, SyncReport
from b2sdk.v2 import KeepOrDeleteMode, CompareVersionMode, NewerFileSyncMode
from pathlib import Path
from time import time
from typing import Generator, Optional
import sys

MAX_WORKERS: int = 10

class IgnoringSyncPolicyManager(SyncPolicyManager):
    def __init__(self, policies_manager: ScanPoliciesManager):
        self._policies_manager = policies_manager

    def get_policy_class(self, sync_type: str, delete: bool, keep_days: bool) -> AbstractFileSyncPolicy:
        def make_policy_class():
            policies_manager = self._policies_manager

            class IgnoringDownAndDeletePolicy(DownAndDeletePolicy):
                def _get_hide_delete_actions(self) -> Generator[AbstractAction, None, None]:
                    def predicate(action):
                        if isinstance(action, LocalDeleteAction):
                            return not policies_manager._exclude_file_set.matches(action.relative_name)
                        return True
                    yield from filter(predicate, super()._get_hide_delete_actions())

            return IgnoringDownAndDeletePolicy

        cls = super().get_policy_class(
            sync_type=sync_type,
            delete=delete,
            keep_days=keep_days)
        return cls if cls is not DownAndDeletePolicy else make_policy_class()

def run_b2_sync(source_path: Path | str, target_path: Path | str, compare_version_mode: CompareVersionMode, ignore_regex: Optional[str], single_threaded: bool, delete: bool, dry_run: bool) -> None:
    def now_millis() -> int:
        return int(round(time() * 1000))

    info = SqliteAccountInfo()
    b2_api = B2Api(info)

    source = parse_folder(str(source_path), b2_api)
    target = parse_folder(str(target_path), b2_api)

    policies_manager = ScanPoliciesManager(
        exclude_all_symlinks=True,
        exclude_file_regexes=[] if ignore_regex is None else [ignore_regex])
    sync_policy_manager = IgnoringSyncPolicyManager(
        policies_manager=policies_manager)

    keep_or_delete_mode = KeepOrDeleteMode.DELETE if delete else KeepOrDeleteMode.NO_DELETE
    synchronizer = Synchronizer(
        policies_manager=policies_manager,
        sync_policy_manager=sync_policy_manager,
        max_workers=1 if single_threaded else MAX_WORKERS,
        dry_run=dry_run,
        allow_empty_source=True,
        compare_version_mode=compare_version_mode,
        compare_threshold=0,
        newer_file_mode=NewerFileSyncMode.SKIP,
        keep_days_or_delete=keep_or_delete_mode)

    with SyncReport(sys.stdout, no_progress=False) as reporter:
        # Warnings about ignore exception thrown in call to sync_folders
        synchronizer.sync_folders(
            source_folder=source,
            dest_folder=target,
            now_millis=now_millis(),
            reporter=reporter)

I don't think this code is doing anything particularly strange.

titus8 commented 2 weeks ago

Is this message a feature of Python 3.13? From https://docs.python.org/3/whatsnew/3.13.html:

io The IOBase finalizer now logs any errors raised by the close() method with sys.unraisablehook. Previously, errors were ignored silently by default, and only logged in Python Development Mode or when using a Python debug build. (Contributed by Victor Stinner in gh-62948.)

I observed the message on b2sdk-2.5.1, but not in b2sdk-2.6.0, so I assumed that b2sdk now avoids the issue. BTW I'm using MacOS Sequoia 15.0.1.

rcook commented 2 weeks ago

@titus8 : Yes, I'm using Python 3.13. I'll update the latest b2sdk. Thanks!

rcook commented 2 weeks ago

This is the commit where this was fixed: cad8b2ba