You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
aiologic.synchronized() as an async-aware alternative to wrapt.synchronized(). Related: GrahamDumpleton/wrapt#236.
aiologic.SimpleLifoQueue as a simplified LIFO queue, i.e. a lightweight alternative to aiologic.LifoQueue without maxsize support.
aiologic.BinarySemaphore and aiologic.BoundedBinarySemaphore as binary semaphores, i.e. semaphores restricted to the values 0 and 1 and using a more efficient implementation.
aiologic.RBarrier as a reusable barrier, i.e. a barrier that can be reset to its initial state (async-aware alternative to threading.Barrier).
aiologic.lowlevel.ThreadLock class (for typing purposes) and aiologic.lowlevel.create_thread_lock() factory function as a new way to obtain unpatched threading.Lock.
aiologic.lowlevel.ThreadRLock class (for typing purposes) and aiologic.lowlevel.create_thread_rlock() factory function as a unique way to obtain unpatched threading.RLock. Solves the problem of using reentrant thread-level locks in the gevent-patched world (due to the fact that threading._PyRLock.__globals__ referenced the patched namespace, which made it impossible to use the original object because it used the patched threading.Lock). Note: like threading._PyRLock, the fallback pure Python implementation is not signal-safe.
aiologic.lowlevel.ThreadOnceLock class (for typing purposes) and aiologic.lowlevel.create_thread_oncelock() factory function as a way to obtain a one-time reentrant lock. The interface mimics that of aiologic.lowlevel.ThreadRLock, but the semantics are different: the first successful release() call, which sets the internal counter to zero, wakes up all threads at once (just like aiologic.Event), and all further acquire() calls become no-ops, which effectively turns the lock into a dummy primitive. And unlike aiologic.lowlevel.ThreadRLock, this primitive is signal-safe, which, combined with the described semantics, makes the primitive suitable for protecting initialization sections.
aiologic.lowlevel.ThreadDummyLock class (for typing purposes) and aiologic.lowlevel.THREAD_DUMMY_LOCK singleton object as a way to obtain a dummy lock.
aiologic.lowlevel.once decorator to ensure that a function is executed only once (inspired by std::sync::Once from Rust). It uses aiologic.lowlevel.ThreadOnceLock under the hood and stores the result in the wrapper's closure, which makes the function both thread-safe and signal-safe when reentrant=True is passed (note: this does not apply to side effects!).
aiologic.lowlevel.lazydeque as a thread-safe/signal-safe wrapper for collections.deque with lazy initialization. It solves the problem of deques' high memory usage: one empty instance of collections.deque takes up 760 bytes on Python 3.11+ (for comparison, one empty list takes up only 56 bytes!). In contrast, one empty instance of aiologic.lowlevel.lazydeque takes up 128 bytes in total, and after initialization (first addition) takes up 832 bytes on Python 3.11+. Free-threading adds an additional 16 bytes in all cases (due to the internal use of aiologic.lowlevel.ThreadOnceLock).
aiologic.lowlevel.lazyqueue as a thread-safe/signal-safe wrapper for _queue.SimpleQueue (when available) or collections.deque with lazy initialization. It provides a non-blocking queue and differs from aiologic.lowlevel.lazydeque in that it is more memory efficient at the cost of less functionality. Instead of 128 and 832 bytes, it takes up 120 and 200 bytes on Python 3.13+.
aiologic.lowlevel.create_green_waiter() and aiologic.lowlevel.create_async_waiter() as functions to create waiters, i.e. new low-level primitives that encapsulate library-specific wait-wake logic. Unlike low-level events, they have no state, and thus have less efficiency for multiple notifications (in particular, they schedule calls regardless of whether the wait has been completed or not). And, of course, for the same reason, they are even less safe (they require more specific conditions for their correct operation).
aiologic.lowlevel.enable_signal_safety() and aiologic.lowlevel.disable_signal_safety() universal decorators to enable and disable signal-safety in the current thread's context. They support awaitable objects, coroutine functions, and green functions, and can be used directly as context managers.
aiologic.lowlevel.signal_safety_enabled() to determine if signal-safety is enabled.
aiologic.lowlevel.create_green_event() and aiologic.lowlevel.create_async_event() as a new way to create low-level events.
aiologic.lowlevel.enable_checkpoints() and aiologic.lowlevel.disable_checkpoints() universal decorators to enable and disable checkpoints in the current thread's context. They support awaitable objects, coroutine functions, and green functions, and can be used directly as context managers.
aiologic.lowlevel.green_checkpoint_enabled() and aiologic.lowlevel.async_checkpoint_enabled() to determine if checkpoints are enabled for the current library.
aiologic.lowlevel.green_checkpoint_if_cancelled() and aiologic.lowlevel.async_checkpoint_if_cancelled(). Currently, aiologic.lowlevel.async_checkpoint_if_cancelled() is equivalent to removed aiologic.lowlevel.checkpoint_if_cancelled(), and aiologic.lowlevel.green_checkpoint_if_cancelled() does nothing. However, these methods have a slightly different meaning: they are intended to accompany aiologic.lowlevel.shield() calls to pre-check for cancellation, and do not guarantee actual checking.
aiologic.lowlevel.green_clock() and aiologic.lowlevel.async_clock() as a way to get the current time according to the current library's internal monotonic clock (useful for sleep-until functions).
aiologic.lowlevel.green_sleep() and aiologic.lowlevel.async_sleep() to suspend the current task for the given number of seconds.
aiologic.lowlevel.green_sleep_until() and aiologic.lowlevel.async_sleep_until() to suspend the current task until the given deadline (relative to the current library's internal monotonic clock).
aiologic.lowlevel.green_sleep_forever() and aiologic.lowlevel.async_sleep_forever() to suspend the current task until an exception occurs.
aiologic.lowlevel.green_seconds_per_sleep() and aiologic.lowlevel.async_seconds_per_sleep() as a way to get the number of seconds during which sleep guarantees exactly one checkpoint. If sleep exceeds this time, it will use multiple calls (to bypass library limitations).
aiologic.lowlevel.green_seconds_per_timeout() and aiologic.lowlevel.async_seconds_per_timeout() that are the same as their sleep equivalents, but for timeouts (applies to low-level waiters/events, as well as all high-level primitives).
copy() method to flags and queues as a way to create a shallow copy without additional imports.
async_borrowed() and green_borrowed() methods to capacity limiters, green_owned() and async_owned() methods to locks. They allow to reliably check if the current task is holding the primitive (or any of its tokens) without importing additional functions.
async_count() and green_count() to reentrant primitives for the same purpose, but returning how many releases need to be made before the primitive is actually released by the current task.
Reentrant primitives can now be acquired and released multiple times in a single call.
Multi-use barriers (cyclic and reusable) can now be used as context managers, which simplifies error handling.
for_() method to condition variables as an async analog of wait_for().
Conditional variables now support user-defined timers. They can be passed to the constructor, called via the timer property, and used to pass the deadline to the notification methods.
Low-level events can now support aiologic.lowlevel.ThreadOnceLock methods by passing locking=True. This allows to synchronize related one-time operations with less memory overhead.
Low-level events can now be shielded from external cancellation by passing shield=True. This allows to implement efficient finalization strategies while preserving the one-time nature of low-level events.
Low-level events can now be forced (like checkpoints) by passing force=True. This allows to use existing event objects instead of checkpoints to minimize possible overhead.
aiologic.lowlevel.SET_EVENT and aiologic.lowlevel.CANCELLED_EVENT as variants of aiologic.lowlevel.DUMMY_EVENT for a set event and a cancelled event respectively. In fact, aiologic.lowlevel.SET_EVENT is just a copy of aiologic.lowlevel.DUMMY_EVENT, but to avoid confusion both variants will coexist (maybe temporarily, maybe not).
aiologic.meta subpackage for metaprogramming purposes.
aiologic.meta.MISSING as a marker for parameters that, when not passed, specify special default behavior.
aiologic.meta.DEFAULT as a marker for parameters with default values.
aiologic.meta.copies() to replace a function with a copy of another.
aiologic.meta.replaces() to replace a function of the same name in a certain namespace.
aiologic.meta.export() to export all module content on behalf of the module itself (by updating __module__).
aiologic.meta.export_deprecated() to export deprecated content via custom __getattr__().
aiologic.meta.await_for() to use awaitable primitives via functions that only accept asynchronous functions.
AIOLOGIC_GREEN_CHECKPOINTS and AIOLOGIC_ASYNC_CHECKPOINTS environment variables.
AIOLOGIC_PERFECT_FAIRNESS environment variable.
Changed
In previous versions, aiologic implicitly provided a strong fairness guarantee, informally called "perfect fairness". The point of this guarantee is to ensure the fairness of wakeups when they are parallel in nature. This had strong effects such as resuming all threads at once (no one is sleeping) on barrier wakeups and deterministic callback scheduling order in event loops when multiple threads call release() at the same time. This behavior is now explicit, optional, and disabled by default when GIL is also disabled. The reason for this change is that the behavior was not efficiently implemented at the Python level via existing atomic operations: deque.remove() increases worst-case time complexity from linear to cubic and gives a noticeable overhead that, in particular, makes barriers significantly slower with a huge number of threads in the free-threaded mode. Nevertheless, with implementation flaws, "perfect fairness" can still be useful, so since this version aiologic provides the AIOLOGIC_PERFECT_FAIRNESS environment variable to explicitly enable or disable it.
To avoid cubic complexity, token removal in perfect fairness style (cannot be disabled in: multi-time events, multi-use barriers, and condition variables) is now synchronized via aiologic.lowlevel.ThreadOnceLock methods in the free-threaded mode.
Signal-safety is now also explicit and can be configured via the universal decorators. When enabled, code behaves as if it were running outside the context in the same thread (as in a separate thread but with the same thread identifiers), allowing both notifying and waiting to be performed safely from inside signal handlers and destructors (affects library detection and low-level waiters).
All timeouts now support the full range of int and float values, and correctly handle NaN and infinity (in particular, timeout=inf is equivalent to timeout=None). A side effect is that large timeouts (exceeding the maximum) increase the number of checkpoints.
All primitives now use aiologic.lowlevel.lazydeque instead of collections.deque to reduce memory usage. This makes their memory usage a bit closer to asyncio primitives (more lightweight than some threading primitives!), and especially affects complex queues, which now consume only ~0.5 KiB instead of ~3 KiB per instance. But the cost of this is synchronization via aiologic.lowlevel.ThreadOnceLock at the first addition to the queue in free-threading.
Shallow copying now relies on the __copy__() method instead of pickle methods. This makes copying faster at the cost of additional overriding in subclasses.
Flags are now a high-level primitive, available as aiologic.Flag, with weakref support.
Reentrant primitives now have checkpoints on reentrant acquires. This should make their behavior more predictable. Previously, checkpoints were not called if a primitive had already been acquired (for performance reasons).
Interfaces and type hints have been improved:
Queues, capacity limiters, and flags are now generic types not only in stubs but also at runtime, making it possible to use subscriptions on Python 3.8. Also, capacity limiters' and flags' type parameters now have default values.
The use of markers as default parameter values has been expanded. None is used when it disables a particular feature (e.g. timeouts or maxsize). aiologic.meta.MISSING is used when it specifies a special default behavior. aiologic.meta.DEFAULT is used when an existing value that is compatible in type will be taken.
aiologic.lowlevel.Event is now a protocol not only in stubs but also at runtime.
Calling flag.set() without arguments is now only allowed for aiologic.lowlevel.Flag[object]. Previously it ignored subscriptions.
aiologic.meta.MissingType is now a subclass of enum.Enum, so static analysis tools now correctly recognize aiologic.meta.MISSING as a singleton instance.
aiologic.lowlevel.current_green_library() and aiologic.lowlevel.current_async_library() now return Optional[str] when passing fallback=True. Previously, str was returned, which was not the expected behavior.
aiologic.lowlevel.AsyncLibraryNotFoundError and aiologic.lowlevel.current_async_library_tlocal are now exactly the same as sniffio.AsyncLibraryNotFoundError and sniffio.thread_local. This allows them to be used interchangeably.
In all modules, type information is now also inline. This allows such information to be used in cases where stubs are not supported. In particular, for sphinx.ext.autodoc. Stubs are still preserved to reduce issues with type checkers.
Overload introspection is now available at runtime on all supported versions of Python.
Thread-related functions have been rewritten:
aiologic.lowlevel.current_thread() now raises RuntimeError for threads started outside of the threading module instead of returning None. This is to prevent situations where the function is used for identification without proper handling of dummy threads, since in such cases dummy threads would share the same "identifier" — None.
The fallback _thread._local implementation now works not only with thread objects but also with main greenlet objects, so it now supports gevent's pool of native worker threads.
Checkpoints have been rewritten:
aiologic.lowlevel.repeat_if_cancelled() has been replaced by aiologic.lowlevel.shield(). Unlike the pre-0.10.0 function of the same name, it is now a universal decorator, and it combines both semantics: it shields both the called function and the calling task from being cancelled. It supports awaitable objects, coroutine functions, and green functions: timeouts are suppressed, and are re-raised after the call completes.
threading checkpoints now use os.sched_yield() (when available) as a way to quickly switch the GIL. This makes them cheaper, but may slightly alter their behavior in free-threading.
They now skip the current library detection when it is not required (for example, when checkpoints are not enabled for any of the imported libraries). This also affects checkpoints in low-level events, which no longer access context variables before using checkpoints, making them much faster.
They can now only be enabled dynamically (without environment variables) at the thread level. This prevents checkpoints from being enabled in created worker threads.
Low-level events have been rewritten:
They are now built on waiters (new low-level primitives), due to which they determine the current library and access the specific API only in wait methods and only if they have not been pre-set. This allows them to be created outside the event loop, and gives more predictable behavior and some performance gains.
They now prevent concurrent calls to the wait method by raising a RuntimeError in such situations, making them safer. Previously, this was not handled in any way on the aiologic side, which required special care and could lead to undefined behavior if the conditions for their use were not met.
Placeholder events now inherit aiologic.lowlevel.GreenEvent and aiologic.lowlevel.AsyncEvent, allowing them to be used in code sections where wait methods are called.
event.is_cancelled() has been renamed to event.cancelled(). This makes them more similar to asyncio futures and thus more familiar to new users.
They are now always cancelled on timeouts, and the event.cancel() method has been removed to avoid redundancy. This should make it easier to work with them outside of aiologic and simplify some things, since now there is no need to call event.cancel(). Previously, green events were not cancelled when a timeout was passed.
They are no longer considered set after being cancelled. This affects the return value of bool(event) and event.is_set(), which now return False for cancelled events.
They now return False after waiting again if they were previously cancelled. Previously True was returned, which could be considered unexpected behavior.
Events have been rewritten:
They no longer save their state when being pickled/copied. So they now share the same behavior as the other synchronization primitives.
The value parameter of aiologic.CountdownEvent has been renamed to initial_value. Accordingly, a property with the same name has also been added.
Barriers have been rewritten:
The parties parameter now has a default value of 0. This allows barriers to be used directly as default factories.
They now allow passing parties equal to 0, with which they ignore the waiting queue length (they only wake up tasks when abort() is called directly or indirectly, e.g. on cancellation or timeout).
Single-use barriers can now be used correctly in a Boolean context: True if the current state is not filling, False otherwise.
They now do not prevent concurrent abort() calls from affecting successful task wakeup. This change is due to the fact that they cannot suppress cancellation in principle, making the prevention of BrokenBarrierError on successful wakeup meaningless. As a consequence, single-use barriers can now be broken after use (e.g. via an abort() call).
Semaphores have been rewritten:
aiologic.Semaphore now disallow passing max_size other than None from subclasses. Previously it was ignored, which could violate user expectations intending to get aiologic.BoundedSemaphore behavior.
aiologic.BoundedSemaphore (and consequently aiologic.Semaphore) now creates aiologic.BoundedBinarySemaphore when max_size <= 1. This makes it possible to use an implementation that is more efficient in both time and memory without importing new classes.
aiologic.BoundedSemaphore.release() now disallows count=0. Previously, it allowed threads to participate in waking up others during race conditions, but aiologic.Semaphore.release() no longer has such semantics.
Capacity limiters have been rewritten:
CapacityLimiter.borrowers is now a read-only property that returns a read-only mapping proxy, which increases safety when working with it.
The total_tokens parameter now has a default value of 1. This allows capacity limiters to be used directly as default factories and makes their interface a bit closer to semaphores.
They can now be used correctly in a Boolean context: True if at least one token has been borrowed, False otherwise.
Locks have been rewritten:
They now set owner (and count) on release rather than on wakeup. This gives the expected values of these parameters when locks are used cooperatively. Previously, it was not possible to determine the next lock owner after release in the same task (it was None).
Condition variables have been rewritten:
They now only support passing low-level (thread-level) locks (synchronous mode), binary semaphores and high-level locks (mixed mode), and None (lockless mode). This change was made to simplify their implementation. For special cases it is recommended to use low-level events directly.
Waiting for a predicate now supports delegating its checking to notifiers and is the default (delegate=True). This reduces the number of context switches to the minimum necessary.
For high-level primitives, all waits are now truly fair and always run with exactly one checkpoint (exactly two in case of cancel), just like all other aiologic functions. This works by using a new reparking mechanism and solves the well-known resource starvation issue.
They now count the number of lock acquires to avoid redundant release() calls when used as context managers. Because of this, they will now never throw a RuntimeError when a wait() call fails (e.g. due to a KeyboardInterrupt while trying to reacquire aiologic.lowlevel.ThreadLock) except in the case of concurrent notify() calls. This makes it safe (with some caveats) to use condition variables even when shielding from external cancellation is not guaranteed.
They now shield lock state restoring not only in async methods, but also in green methods. It is still not guaranteed that a wait() call cannot be cancelled in any unpredictable way (e.g. when a greenlet is killed by an exception other than GreenletExit), but now condition variables can be safely used in more scenarios.
They now check that the current task is the owner of the lock before starting the wait. This corresponds to the behavior of the standard condition variables, and allows exceptions to be raised with more meaningful messages.
They now check that the current task is the owner of the lock before starting notification for predicates for all variations, and in general for any calls for high-level primitives. While this change reduces the number of scenarios in which condition variables can be used, it provides the thread-safety needed for the new features to work.
They now always yield themselves when used as context managers. This diverges from the standard condition variables, but makes the interface more consistent.
They now return the original value of the predicate, allowing the wait_for() and for_() methods to be used in more scenarios. Previously, a value of type bool was returned.
They are now more accurately interpreted as bool. For locks from the threading module, True is returned if the lock is locked and False otherwise, which matches the behavior of locks from the aiologic module. For None, False is always returned. With this change, condition variables can now be used to determine the status of an operation (just like normal aiologic locks) regardless of the lock used. Previously, True was always returned for both cases.
Resource guards have been rewritten:
They are now interpreted in a Boolean context in the same way as locks. Previously, they returned opposite values.
Queues have been rewritten:
They no longer support the _qsize() (returns queue size) and _items() (returns a list of queue items) overrides, and they have been removed accordingly. Unlike the other overridden methods, they required thread-safety on the user side, which could cause additional difficulties. It is now recommended to use queues from culsans, a derivative of aiologic, to create full-featured custom queues.
The representation of primitives has been changed. All instances now include the module name, use the correct class name in subclasses (except for private classes) and show their status in representation.
aiologic.lowlevel.current_green_token() now returns the current thread, and aiologic.lowlevel.current_green_token_ident() now uses the current thread ID for threading. This makes these functions more meaningful, and leads to the expected behavior in group-level locks. Previously, constant values were returned for threading.
aiologic.lowlevel.current_green_token() and aiologic.lowlevel.current_green_task() now return main greenlet objects for gevent's pool of native worker threads (and for any dummy threads when greenlet is imported). This allows these functions to be used with gevent without additional handlers.
The first aiologic.lowlevel.current_thread() call no longer patches the threading module for PyPy (to fix the race in Thread.join()). This is done to eliminate side effects and possible conflicts with debuggers. Use PyPy 7.3.18 or higher instead, or apply a separate patch yourself.
The first aiologic.lowlevel.GreenEvent instantiation no longer injects destroy() into eventlet hubs. This is delegated to a separate patch as it gives more predictable behavior. However, aiologic still injects schedule_call_threadsafe() since eventlet/eventlet#1023 is still unresolved.
curio events now use lockless futures instead of concurrent.futures.Future. This makes the implementation of curio support completely non-blocking (like the rest of the concurrency libraries), which has a positive impact on performance.
sniffio is now a required dependency. This is done to simplify the code logic (which previously treated sniffio as an optional dependency) and should not introduce any additional complexity.
typing-extensions is now a required dependency on Python < 3.13. This is done to make it easier to work with stubs and use new features (such as warnings.deprecated) on older versions of Python.
The build system has been changed from setuptools to uv + hatch. It keeps the same pyproject.toml format, but has better performance, better logging, and builds cleaner source distributions (without setup.cfg). Dependencies specific to the development process have been redefined using PEP 735.
The version identifier is now generated dynamically and includes the latest commit information for development versions, which simplifies bug reporting. It is also passed to archives generated by GitHub (via .git_archival.txt) and source distributions (via PKG-INFO).
Deprecated
timeout<0 in all primitives, as they differ from the semantics of the standard library, which could lead to their incorrect use (since they are equivalent timeout=0, but not to no timeout).
action as a positional parameter in aiologic.ResourceGuard in favor of using it as a keyword-only parameter.
maxsize<=0 in complex queue constructors in favor of maxsize=None: support for maxsize<0 is not pythonic, goes against common style, and maxsize=0 may in the future be used to create special empty queues.
aiologic.PLock in favor of aiologic.BinarySemaphore.
aiologic.RLock.level in favor of aiologic.RLock.count.
aiologic.lowlevel.MISSING in favor of aiologic.meta.MISSING.
aiologic.lowlevel.GreenEvent and aiologic.lowlevel.AsyncEvent direct creation in favor of aiologic.lowlevel.create_green_event() and aiologic.lowlevel.create_async_event(): they will become protocols in the future.
aiologic.lowlevel.Flag in favor of aiologic.Flag.
aiologic.lowlevel.checkpoint() in favor of aiologic.lowlevel.async_checkpoint() (previously alias): checkpoints are now strictly separated into green and async checkpoints.
Removed
aiologic.CapacityLimiter.*_on_behalf_of() methods: they did not provide the proper thread-safety level (capacity limiters need to be higher-level primitives for this), but also made the implementation more complex and thus degraded performance.
is_set parameter from one-time and reusable events (aiologic.Event and aiologic.REvent).
aiologic.lowlevel.<library>_running(): these functions have not been used and could be misleading.
aiologic.lowlevel.checkpoint_if_cancelled() and aiologic.lowlevel.cancel_shielded_checkpoint(): they only supported asynchronous libraries and were not actually used in high-level primitives.
aiologic.lowlevel.<library>_checkpoints_cvar in favor of aiologic.lowlevel.enable_checkpoints() and aiologic.lowlevel.disable_checkpoints().
AIOLOGIC_GREEN_LIBRARY and AIOLOGIC_ASYNC_LIBRARY environment variables: they could be confusing because they did not affect sniffio.current_async_library(). The alternative of setting the default directly in sniffio (via sniffio.thread_local.__class__.name) affects anyio, which refuses to make a successful anyio.run() call when the current async library is set.
Fixed
Slotted classes lacked __getstate__(), which caused internal fields to be copied during pickling even if __getnewargs__() was defined. As a result, it was impossible to copy primitives while using them.
In aiologic.lowlevel.shield() (previously aiologic.lowlevel.repeat_if_cancelled()):
Hangs could occur when using anyio.CancelScope() with the asyncio backend. Now this case is handled in a special way. Related: agronholm/anyio#884.
Reference cycles were possible when another exception was raised after a asyncio.CancelledError was caught: in this case, the last asyncio.CancelledError was not removed from the frame.
aiologic.lowlevel.current_thread() returned:
another main thread object after monkey patching the threading module with eventlet (now the same as from threading.main_thread()).
None for the main thread after monkey patching the threading module with gevent (now the same as from threading.main_thread()).
green thread objects for dummy threads whose identifier matched the running greenlets (now raises an exception according to the new behavior).
The initialization of low-level waiter classes was protected from concurrent execution using a mutex, which could lead to deadlock if it was interrupted and retried in the same thread. Now, aiologic.lowlevel.ThreadOnceLock is used for this (via aiologic.lowlevel.once()), which ensures signal-safety.
Using aiologic.RLock from inside a signal handler or destructor could result in a false release if the execution occurred inside an *_acquire() call after setting the owner property but before setting the count property. The order of operations is now inverted. This makes aiologic.RLock a bit more signal-safe than threading._PyRLock.
Using checkpoints for threading could cause hub spawning in worker threads when aiologic is imported after monkey patching the time module with eventlet or gevent. As a result, the open files limit could have been exceeded.
Blocking eventlet calls did not check the context, which could lead to incorrect behavior when executing blocking calls in the hub context (as part of scheduled calls).
The locks and semaphores (and the capacity limiters and simple queues based on them) did not handle exceptions at checkpoints, so that cancelling at checkpoints (trio case by default) did not release the primitive.
The methods of complex queues (all except aiologic.SimpleQueue) used an incorrect condition for cancellation handling, which could break thread-safety after cancellation. Now the handling is changed to match that of locks and semaphores, which additionally speeds up methods by reducing operations.
Complex queues did not remove the event from the secondary internal queue on unsuccessful cancellation, which could lead to memory leaks in some situations.
In very rare cases, curio events would set the future attribute after the set() method was completed and thus cause a hang.
In very rare cases, lock acquiring methods did not notify newcomers due to calling a non-existent method when racing during cancellation, causing a hang (0.14.0 regression).
A non-existent function was imported for trio tokens, which resulted in inability to use aiologic.lowlevel.current_async_token() and aiologic.lowlevel.current_async_token_ident() for trio (0.14.0 regression).
The _local class was imported directly from the _thread module, which caused the current library to be set only for the current greenlet and not for the whole thread after monkey patching (0.14.0 regression).
Semaphores with value > 1 incorrectly handled the optimistic acquire case after adding an event to the queue, resulting in excessive decrements in a free-threading mode (0.2.0 regression).