Skip to content

What is needed to support the free-threaded build? #1956

@ngoldbaum

Description

@ngoldbaum

See also #1649

Using the latest version of Cython, PyAV builds and passes all tests on free-threaded Python 3.13.5 on my Mac if I force the GIL to be disabled. If I additionally run the tests many times in a per-test thread pool using pytest-run-parallel, I do see some failures in test_logging, but I think that's due to use of global logging state rather than real thread safety issues:

=================================================== ERRORS ==================================================== ___________________________________ ERROR at call of test_threaded_captures ___________________________________
def test_threaded_captures() -> None:
    av.logging.set_level(av.logging.VERBOSE)

    with av.logging.Capture(local=True) as logs:
        do_log("main")
        thread = threading.Thread(target=do_log, args=("thread",))
        thread.start()
        thread.join()
  assert (av.logging.INFO, "test", "main") in logs

E AssertionError: assert (32, 'test', 'main') in []

tests/test_logging.py:31: AssertionError
____________________________________ ERROR at call of test_global_captures ____________________________________

def test_global_captures() -> None:
    av.logging.set_level(av.logging.VERBOSE)

    with av.logging.Capture(local=False) as logs:
        do_log("main")
        thread = threading.Thread(target=do_log, args=("thread",))
        thread.start()
        thread.join()
  assert (av.logging.INFO, "test", "main") in logs

E AssertionError: assert (32, 'test', 'main') in []

tests/test_logging.py:44: AssertionError
________________________________________ ERROR at call of test_repeats ________________________________________

def test_repeats() -> None:
    av.logging.set_level(av.logging.VERBOSE)

    with av.logging.Capture() as logs:
        do_log("foo")
        do_log("foo")
        do_log("bar")
        do_log("bar")
        do_log("bar")
        do_log("baz")

    logs = [log for log in logs if log[1] == "test"]
  assert logs == [
        (av.logging.INFO, "test", "foo"),
        (av.logging.INFO, "test", "foo"),
        (av.logging.INFO, "test", "bar"),
        (av.logging.INFO, "test", "bar (repeated 2 more times)"),
        (av.logging.INFO, "test", "baz"),
    ]

E AssertionError: assert [(32, 'test',...test', 'baz')] == [(32, 'test',...test', 'baz')]
E At index 0 diff: (32, 'test', 'bar') != (32, 'test', 'foo')
E Left contains one more item: (32, 'test', 'baz')
E Use -v to get more diff

tests/test_logging.py:62: AssertionError
_________________________________________ ERROR at call of test_error _________________________________________

def test_error() -> None:
    av.logging.set_level(av.logging.VERBOSE)

    log = (av.logging.ERROR, "test", "This is a test.")
    av.logging.log(*log)
    try:
      av.error.err_check(-errno.EPERM)

tests/test_logging.py:79:


av/error.pyx:385: in av.error.err_check
cpdef int err_check(int res, filename=None) except -1:


raise cls(code, message, filename, log)
E av.error.PermissionError: [Errno 1] Operation not permitted

av/error.pyx:424: PermissionError

During handling of the above exception, another exception occurred:

def test_error() -> None:
    av.logging.set_level(av.logging.VERBOSE)

    log = (av.logging.ERROR, "test", "This is a test.")
    av.logging.log(*log)
    try:
        av.error.err_check(-errno.EPERM)
    except av.error.PermissionError as e:
      assert e.log == log

E AssertionError: assert None == (16, 'test', 'This is a test.')
E + where None = PermissionError(1, 'Operation not permitted').log

tests/test_logging.py:81: AssertionError
---------------------------------------------- Captured log call ----------------------------------------------
ERROR libav.test:test_logging.py:77 This is a test.
***************************************** pytest-run-parallel report ******************************************
All tests were run in parallel! 🎉
=========================================== short test summary info ===========================================
PARALLEL FAILED tests/test_logging.py::test_threaded_captures - AssertionError: assert (32, 'test', 'main') in []
PARALLEL FAILED tests/test_logging.py::test_global_captures - AssertionError: assert (32, 'test', 'main') in []
PARALLEL FAILED tests/test_logging.py::test_repeats - AssertionError: assert [(32, 'test',...test', 'baz')] == [(32, 'test',...test', 'baz')]
PARALLEL FAILED tests/test_logging.py::test_error - AssertionError: assert None == (16, 'test', 'This is a test.')
================================== 286 passed, 13 skipped, 4 errors in 7.62s ==================================

It looks like both test_logging.py and test_timeout.py have tests that create Python threads, but I don't see anything like a parallel processing stress test using multiprocessing or threading. I also don't see any thread safety guarantees in the docs.

What are the thread safety guarantees that you're interested in guaranteeing? Presumably it should be safe to simultaneously read from streams in multiple threads, but then is there any sort of protection from someone abusing threads to simultaneously read and mutate a libav stream? If not, is that also an issue on the GIL-enabled build?

Ultimately I'd like to add support for the free-threaded build following the guidance in the Cython documentation:

https://cython.readthedocs.io/en/latest/src/userguide/freethreading.html

I'd also like to enable free-threaded wheel builds in the cibuildwheel configuration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions