/*** Copyright (C) 2022 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: IAsyncTransaction.hpp Date: 2022-2-14 Author: Reece ***/ #pragma once namespace Aurora::IO::Loop { struct ILoopSource; } namespace Aurora::IO::CompletionGroup { struct ICompletionGroup; } namespace Aurora::Async { AUKN_SYM AuSPtr GetSelfIOGroup(); } #include "CompletionGroup/ICompletionGroupWorkHandle.hpp" #include namespace Aurora::IO { /** * @brief Cross-platform reusable overlapped-based IO transaction * * @warning this object is bound by the current thread at time of creation. * Do not access this object across threads as there is no MT guarantee on this particular object * Using the synchronization primitives, loop or authreading, is fine. */ // Thread-Local! struct IAsyncTransaction { virtual bool StartRead(AuUInt64 uOffset, const Memory::MemoryViewWrite &memoryView) = 0; virtual bool StartWrite(AuUInt64 uOffset, const Memory::MemoryViewRead &memoryView) = 0; /** * @brief "Non-blocking" is-signaled and call any callback routines (similar to nt alertable sleeps of a period zero) */ virtual bool Complete() = 0; /** * @brief Block for completion and call any callback routines (similar to nt alertable sleeps) */ virtual bool Wait(AuUInt32 uTimeout) = 0; /** * @brief Non-blocking has-failed (no callbacks, no alert sleep) * @warning Also note that we generally try to suppress end of stream "errors" * @warning If the underlying stream is empty or at the EoS/F point, expect a successful read of zero at least once. * Further reads may fail. * * This design philosophy extends to networking whereby various socket shutdown conditions that most applications would * consider to be an unsafe shutdown are suppressed as non-fatal. Not that you can't manually query the last error, * it's just that we shouldn't be generating massive errors logs and presenting stupid warnings to the client, over so * called "errors" that merely constitute somebodys mother vacuuming over an ethernet cable or a cell tower falling out of * range. In my estimation, IO errors and other high level errors as presented to an application should only be that of * logical errors, or that of us bailing out of an unsafe condition, or perhaps the kernel crying about IO resources. Some * smart-ass networking protocol being aware of an incomplete socket shutdown isn't worth noting, unless the user has * explicit wishes to log the exact reason of socket shutdown (ie: ISocket::GetError() under ISocketDriver::OnEnd) * * Likewise, this reasoning extends to IPC pipes; pipe breakage or other EoS condition should not result in an error. * @return */ virtual bool HasFailed() = 0; /** * @brief Non-blocking is-signaled (...and dispatched via ::Complete() or other IO yield) (no callbacks, no alert sleep) * @return */ virtual bool HasCompleted() = 0; /** * @brief * @return */ virtual AuUInt GetOSErrorCode() = 0; /** * @brief Returns the last packets length assuming ::Complete() is true or you are within the registered callback */ virtual AuUInt32 GetLastPacketLength() = 0; /** * @brief Registers an NT-like APC callback for the IO transaction. * Can be executed under any Aurora loop subsystem sleep */ virtual void SetCallback(const AuSPtr &pSubscriber) = 0; /** * @brief Provides a loop source that becomes signaled once the transaction is complete. * Polling the transaction may result in the execution of the callback. * * @warning This function needs refactoring from "New..." to "To..." * @warning This loop source may be that of the CompletionGroup::ICompletionGroup::ToAnyLoopSource(), if provided via TryAttachToCompletionGroup * @warning Remember to never provide the same loop source twice to the sam LoopQueue or to sleep twice on the same loop source using AuAsync * * To prevent logicial errors, you should only be sleeping on this loop source within your scope rather than delegating it a ILoopQueue or AuAsync to prevent double attaches * EG: pThat->NewLoopSource()->WaitOn(5 /\*ms*\/); * This is just a recommendation. If you are yielding on only a few file streams and can guarantee you aren't registering the source twice on a single LoopQueue, it's fine. * * If you want to ensure proper batching with minimal IO resources, and safe AuAsync interop, you should probably just use CompletionGroups */ virtual AuSPtr NewLoopSource() = 0; /** * @brief Resets ::Complete() and NewLoopSource()->IsSignaled() to false */ virtual void Reset() = 0; virtual void SetBaseOffset(AuUInt64 uBaseOffset) = 0; virtual bool TryAttachToCompletionGroup(const AuSPtr &pCompletionGroup) = 0; virtual CompletionGroup::ICompletionGroupWorkHandle *ToCompletionGroupHandle() = 0; virtual AuSPtr GetCompletionGroup() = 0; AURT_ADD_USR_DATA_EXP(this->ToCompletionGroupHandle()); }; inline bool ReadAsync(const AuSPtr &pTransaction, AuUInt64 uOffset, const Memory::MemoryViewWrite &memoryView, AuOptional> callback = {}, AuOptional> pCompletionGroup = { Aurora::Async::GetSelfIOGroup() }) { SysCheckArgNotNull(pTransaction, false); if (pCompletionGroup && pCompletionGroup.Value()) { (void)pTransaction->TryAttachToCompletionGroup(pCompletionGroup.Value()); } if (callback) { auto pRet = AuMakeShared(); SysCheckNotNullMemory(pRet, false); pRet->OnAsyncFileOpFinishedFunctional = callback.Value(); pTransaction->SetCallback(pRet); } return pTransaction->StartRead(uOffset, memoryView); } inline bool WriteAsync(const AuSPtr &pTransaction, AuUInt64 uOffset, const Memory::MemoryViewRead &memoryView, AuOptional> callback = {}, AuOptional> pCompletionGroup = { Aurora::Async::GetSelfIOGroup() }) { SysCheckArgNotNull(pTransaction, false); if (pCompletionGroup && pCompletionGroup.Value()) { (void)pTransaction->TryAttachToCompletionGroup(pCompletionGroup.Value()); } if (callback) { auto pRet = AuMakeShared(); SysCheckNotNullMemory(pRet, false); pRet->OnAsyncFileOpFinishedFunctional = callback.Value(); pTransaction->SetCallback(pRet); } return pTransaction->StartWrite(uOffset, memoryView); } }