/*** Copyright (C) 2022 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: ILoopQueue.hpp Date: 2022-2-12 Author: Reece ***/ #pragma once namespace Aurora::Loop { // ILoopQueue's are comparable to a CoreFoundation RunLoop with the input parameters of NT's WaitMultipleObjects and RegisterWaitForSingleObject // This shouldn't be too much heavier than CF's libevent/NT abstraction struct ILoopQueue { /** * @brief * [*] Source add/remove operations should be followed by a Commit() to commit the changes * * [*] Calling Source[Add/Remove] during a Wait[Any/All[Ex]] will cause the wait function to * return false, allowing for the caller to rewait on the updated queue, or to allow * the thread to account for the object-was-invalidated condition * * @param source * @return * @note thread safe / nonblocking | can be called alongside any other function marked as such * @warning it is unsafe to add an ILoopSource more than once. */ virtual bool SourceAdd(const AuSPtr &source) = 0; /** * @brief Same behaviour as SourceAdd * Note: Timeout is specified in MS and polled during WaitAny/All failure or completition * Objects awaiting timeout will not preempt the loop * @param source * @param timeoutMS * @return * @note thread safe / nonblocking | can be called alongside any other function marked as such * @warning it is unsafe to add an ILoopSource more than once. */ virtual bool SourceAddWithTimeout(const AuSPtr &source, AuUInt32 timeoutMS) = 0; /** * @brief Remove operations include commit. You do not need to follow a remove with a commit. * Sources are defacto poped unless subscriber returns false indicating repeated lock attempts are wanted. * Should no subscriber be registered, the loop source will not be automatically removed * @param source * @note thread safe / nonblocking | can be called alongside any other function marked as such */ virtual bool SourceRemove(const AuSPtr &source) = 0; /** * @brief Updates the OS watchdog list cache concept after Source[Remove/Add[WithTimeout]] * Commits may occur while another thread is waiting on the loop queue. * In those circumstances, they are internally preempted and rescheduled. * WaitAnd has undefined behaviour across MT commit. Linux *might* work. * NT wont until an APC-as-a-preempt-signal hack is implemented. * Rare edge case for all i care - it'll remain a blocking edge case for.now. * @return * @note thread safe */ virtual bool Commit() = 0; /** * @brief * @return the amount of loop sources added to the queue * @note thread safe | can be called alongside any other function marked as such */ virtual AuUInt32 GetSourceCount() = 0; /** * @brief Appends a callback handler to be paired with a loop source * * Once a loop source become signaled, and a Wait[Any/All] function call is made, loop source subscribers are called * to filter the final should-remove operation. The final popped objects are returned from Wait[Any[Ex]] and forgotten. * * If no ILoopSourceSubscribers are registered, the edge case behaviour is that loop sources will not be automatically * dequeued. No user code explicitly told us to do anything other than wait, so the default behaviour of Wait[Any[Ex]] * shall be to not evict loop sources, until at least one callback is registered, or until SourceRemove is called. * * Returning true from a ILoopSourceSubscriber will allow the ILoopQueue to evict the loop source and callback. A call to ::Commit is not required. * Returning false from a ILoopSourceSubscriber will prevent eviction, resulting in continued ILoopSourceSubscriber callbacks to other subscribers * * Once all callbacks are evicited, loop sources are automatically dequeued; however, objects without a callback shall not be dequeued. * * @param source * @param subscriber * @return * @note thread safe | can be called alongside any other function marked as such */ virtual bool AddCallback(const AuSPtr &source, const AuSPtr &subscriber) = 0; /** * @brief Identical behaviour to that of the basic method. In addition, ILoopSourceSubscriberEx::OnTimeout is called * on violation of the timeout provided to SourceAddWithTimeout, and the OnFinished method has been extended to * accept a position number. This is the position of the subscriber relative to the signal-state change tick. * If the loop source is singleshot, and multiple callbacks are registered, all callbacks will be called with * a position counter. Furthermore, extended callbacks are called __before__ non-ex variants. * @param source * @param subscriber * @return * @note thread safe | can be called alongside any other function marked as such */ virtual bool AddCallbackEx(const AuSPtr &source, const AuSPtr &subscriber) = 0; /** * @brief * Registers a callback to be called when either, an loop source has no callbacks, or a loop source could be dequeued. * Returning false from OnFinished shall prevent the loop queue from decommitting the loop source. * @param subscriber * @return * @note thread safe | can be called alongside any other function marked as such */ virtual bool AddCallback(const AuSPtr &subscriber) = 0; /** * @brief Nonblocking check for alert objects in the loop queue * @return * @note thread safe / nonblocking | can be called alongside any other function marked as such */ virtual bool IsSignaledPeek() = 0; /** * @brief Nonblocking wait-any for all objects in the loop queue * @warning may yield to ILoopSourceSubscriber delegate on the current context * @warning (technically unsafe, use alloc-unsafe extended version to acknowledge unlocked sources) * @return * @note thread safe / nonblocking | can be called alongside any other function marked as such */ virtual AuUInt32 PumpNonblocking() = 0; /** * @brief Nonblocking wait-any for all objects in the loop queue * @warning may yield to ILoopSourceSubscriber delegate on the current context * @return * @note thread safe / nonblocking | can be called alongside any other function marked as such */ virtual AuList> PumpNonblockingEx() = 0; /** * @brief Waits on all the submitted loop sources until they are all complete or until the timeout has finished. * Note: the completion of another Wait[All/Any[Ex]] call may result in a * @param timeout timeout in MS, zero = indefinite * @return * @warning (may yield to ILoopSourceSubscriber delegate on the current context) * @warning (thread safety might be limited to blocking callers of the object) */ virtual bool WaitAll (AuUInt32 timeout = 0) = 0; /** * @brief Waits on all the loop sources until at least one is signaled. * @param timeout timeout in MS, zero = indefinite. * Use IsSignaledPump for nonblocking. * @return * @warning (may yield to ILoopSourceSubscriber delegate on the current context) * @warning (technically unsafe, use alloc-unsafe extended version to acknowledge unlocked sources) * @note thread safe | can be called alongside any other function marked as such */ virtual AuUInt32 WaitAny (AuUInt32 timeout = 0) = 0; /** * @brief Waits on all the loop sources until at least one is signaled. * @param timeout timeout in MS, zero = indefinite. * Use IsSignaledPump for nonblocking. * @return * @warning (may yield to ILoopSourceSubscriber delegate on the current context) * @note thread safe | can be called alongside any other function marked as such */ virtual AuList> WaitAnyEx(AuUInt32 timeout = 0) = 0; /** * @brief * Mostly relevant on Windows where the limit is 64 versus horrible UNIX-y socket * optimized interfaces that can handle thousands of fds * * In large loop queues, a maximum of * `sectionTickTime * (sectionDequeCount + clamp(platformSanity, sectionDequeCount))` * kernel objects can be checked within a given time frame. * * What this effectively does is iterate over a subdivided work queue, aligned * to platformSanity (NT: 64); [i, i+platform sanity] objects can then be waited on. * * On timeout of sectionTickTime, * [i, i+sectionDequeCount] of the tick could be checked, however, we know * [i, i+platform sanity] timeouted so, in the event of a timeout, we can * asynchronously check [i+platform sanity, ...] to be nicer faster. Since * we've already spent the maximum allocated time to poll on a mere subset, * we may as well desperately check sectionDequeCount worth of objects, and * write-off the time spent blocking. That way, we get upto platform sanity * of is-signaled checks for free in the worst case scenario. The perspective * that the overshoot is expensive doesn't make sense. The is-signaled * check is basically free and the initial timeout/yield-to-kern over set 0 * was the bottleneck * * Otherwise on success, we know to check in the range of [i, platform sanity * for at least one alerted object. * * Internal logic will dequeue platform coefficient aligned handles, and the timeout * optimization shall check [i+platform sanity, i+platform sanity+sectionDequeCount]. * 1 handle =/= 1 loop source, tho usually 1 loop source ~= 1 handle * * tl'dr: * [*] dont use if you cant stall for sectionTickTime if sectionDequeCount of your * loop sources are unsignaled * [*] chug paths are intended for observer threads and not for maintaining high * performance. no sane traditional application would ever have 64+ signalable * objects stuck in a loop. though if you do, and you dont configure anything, * the worst case scenario is, WaitAny can take upto a millisecond to discover * a signaled loop source. * * @param sectionTickTime * @param sectionDequeCount */ virtual void ChugPathConfigure(AuUInt32 sectionTickTime, AuSInt sectionDequeCount) = 0; /** * @brief Hints that the calling program understands the kernel shouldnt schedule tne entire source list, and instead, we should long poll */ virtual void ChugHint(bool value) = 0; }; AUKN_SYM AuSPtr NewLoopQueue(); }