/** * This should be considered an internal class and a detail of * implementation. * An object that is created when user tries to query database. * It contains a delegate `connect()` that will be called when query is * completed and will self-destruct afterwards. Concrete delegates are * declared in child classes of this `DBTask`, since they can have different * signatures, depending on the query. * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class DBTask extends AcediaObject dependson(Database) abstract; /** * Life of instances of this class is supposed to go like so: * 1. Get created and returned to the user that made database query so * that he can setup a delegate that will receive the result; * 2. Wait until database query result is ready AND all previous tasks * have completed; * 3. Call it's `connect()` delegate with query results; * 4. Deallocate itself. * * Task is determined ready when it's `DBQueryResult` variable was set. * * This class IS NOT supposed to be accessed by user at all - this is simply * an auxiliary construction that allows us to make calls to the database * like so: `db.ReadData(...).connect = handler;`. * * Since every query can have it's own set of returning parameters - * signature of `connect()` method can vary from task to task. * For this reason we define it in child classes of `BDTask` that specialize in * particular query. */ var private DBTask previousTask; // These allows us to detect when previous task got completed (deallocated) var private int previousTaskLifeVersion; var private Database.DBQueryResult taskResult; var private bool isReadyToComplete; var private LoggerAPI.Definition errLoopInTaskChain; protected function Finalizer() { if (previousTask != none) { previousTask.FreeSelf(previousTaskLifeVersion); } previousTask = none; previousTaskLifeVersion = -1; isReadyToComplete = false; } /** * Sets `DBQueryResult` for the caller task. * * Having previous task assigned is not required for the caller task to * be completed, since it can be the first task. * * @param task Task that has to be completed before this one can. */ public final function SetPreviousTask(DBTask task) { previousTask = task; if (previousTask != none) { previousTaskLifeVersion = previousTask.GetLifeVersion(); } } /** * Returns `DBQueryResult` assigned to the caller `DBTask`. * * This method should only be called after `SetResult()`, otherwise it's * behavior and return result should be considered undefined. * * @return `DBQueryResult` assigned to the caller `DBTask`. */ public final function Database.DBQueryResult GetResult() { return taskResult; } /** * Assigns `DBQueryResult` for the caller task. * * Every single task has to be assigned one and cannot be completed before * it does. * * This value can be assigned several times and the last assigned value will * be used. * * @param result Result of the query, relevant to the caller task. */ public final function SetResult(Database.DBQueryResult result) { taskResult = result; isReadyToComplete = true; } /** * Override this to call `connect()` delegate declared in child classes. * Since this base class does not itself have `connect()` delegate declared - * this method cannot be implemented here. */ protected function CompleteSelf() {} /** * Attempts to complete this task. * Can only succeed iff caller task both has necessary data to complete it's * query and all previous tasks have completed. */ public final function TryCompleting() { local int i; local array tasksQueue; tasksQueue = BuildRequiredTasksQueue(); // Queue is built backwards: tasks that have to be completed first are // at the end of the array for (i = tasksQueue.length - 1; i >= 0; i -= 1) { if (tasksQueue[i].isReadyToComplete) { tasksQueue[i].CompleteSelf(); _.memory.Free(tasksQueue[i]); } else { break; } } } // We do not know how deep `previousTask`-based chain will go, so we // will store tasks that have to complete last earlier in the array. private final function array BuildRequiredTasksQueue() { local int i; local int expectedLifeVersion; local bool loopDetected; local DBTask nextRequiredTask; local array tasksQueue; nextRequiredTask = self; tasksQueue[0] = nextRequiredTask; while (nextRequiredTask.previousTask != none) { expectedLifeVersion = nextRequiredTask.previousTaskLifeVersion; nextRequiredTask = nextRequiredTask.previousTask; if (nextRequiredTask.GetLifeVersion() != expectedLifeVersion) { break; } for (i = 0; i < tasksQueue.length; i += 1) { if (nextRequiredTask == tasksQueue[i]) { loopDetected = true; break; } } if (!loopDetected) { tasksQueue[tasksQueue.length] = nextRequiredTask; } else { _.logger.Auto(errLoopInTaskChain).ArgClass(nextRequiredTask.class); break; } } return tasksQueue; } defaultproperties { errLoopInTaskChain = (l=LOG_Error,m="`DBTask` of class `%1` required itself to complete. This might cause database to get damaged unexpectedly. Please report this to the developer.") }