1c3437056SNickeau<?php 2c3437056SNickeau 3c3437056SNickeau 4c3437056SNickeaunamespace ComboStrap; 5c3437056SNickeau 6c3437056SNickeau/** 7c3437056SNickeau * Class Event 8c3437056SNickeau * @package ComboStrap 9c3437056SNickeau * Asynchronous pub/sub system 10c3437056SNickeau * 11c3437056SNickeau * Dokuwiki allows event but they are synchronous 12c3437056SNickeau * because php does not live in multiple thread 13c3437056SNickeau * 14c3437056SNickeau * With the help of Sqlite, we make them asynchronous 15c3437056SNickeau */ 16c3437056SNickeauclass Event 17c3437056SNickeau{ 18c3437056SNickeau 19c3437056SNickeau const EVENT_TABLE_NAME = "EVENTS_QUEUE"; 20c3437056SNickeau 219fc13f00Sgerardnico const CANONICAL = "event"; 22b0217656Sgerardnico 23b0217656Sgerardnico /** 24b0217656Sgerardnico * Uppercase mandatory (the column is uppercased when returnd from a *) 25b0217656Sgerardnico */ 26b0217656Sgerardnico const EVENT_NAME_ATTRIBUTE = "NAME"; 27b0217656Sgerardnico 28b0217656Sgerardnico /** 29b0217656Sgerardnico * Uppercase mandatory (the column is uppercased when returnd from a *) 30b0217656Sgerardnico */ 31b0217656Sgerardnico const EVENT_DATA_ATTRIBUTE = "DATA"; 32b0217656Sgerardnico /** 33b0217656Sgerardnico * Uppercase mandatory (the column is uppercased when returnd from a *) 34b0217656Sgerardnico */ 35b0217656Sgerardnico const TIMESTAMP_ATTRIBUTE = "TIMESTAMP"; 36c3437056SNickeau 37c3437056SNickeau /** 38c3437056SNickeau * process all replication request, created with {@link Event::createEvent()} 39c3437056SNickeau * 40c3437056SNickeau * by default, there is 5 pages in a default dokuwiki installation in the wiki namespace) 41b0217656Sgerardnico * 42b0217656Sgerardnico * @param int $maxEvent In case of a start or if there is a recursive bug. We don't want to take all the resources 43b0217656Sgerardnico * 44c3437056SNickeau */ 45b0217656Sgerardnico public static function dispatchEvent(int $maxEvent = 10) 46c3437056SNickeau { 47*4d15adccSNico $comboFunctionName = 'ComboDispatchEvent'; 48*4d15adccSNico print "$comboFunctionName(): Trying to get a lock" . NL; 490360a848Sgerardnico $lock = self::getLock(); 50a6d63b89Sgerardnico try { 51a6d63b89Sgerardnico $lock->acquire(); 52a6d63b89Sgerardnico } catch (ExceptionTimeOut $e) { 53a6d63b89Sgerardnico // process running 54a6d63b89Sgerardnico return; 55a6d63b89Sgerardnico } 56*4d15adccSNico print "$comboFunctionName(): Locked" . NL; 57a6d63b89Sgerardnico 58a6d63b89Sgerardnico try { 5904fd306cSNickeau try { 60c3437056SNickeau $sqlite = Sqlite::createOrGetBackendSqlite(); 6104fd306cSNickeau } catch (ExceptionSqliteNotAvailable $e) { 6204fd306cSNickeau LogUtility::error("Sqlite is mandatory for asynchronous event", self::CANONICAL, $e); 63c3437056SNickeau return; 64c3437056SNickeau } 65c3437056SNickeau 6670bbd7f1Sgerardnico 67c3437056SNickeau $rows = []; 68b0217656Sgerardnico /** 69b0217656Sgerardnico * Returning clause 70b0217656Sgerardnico * does not work 71b0217656Sgerardnico */ 72b0217656Sgerardnico $version = $sqlite->getVersion(); 73b0217656Sgerardnico if ($version > "3.35.0") { 74b0217656Sgerardnico 75b0217656Sgerardnico // returning clause is available since 3.35 on delete 76b0217656Sgerardnico // https://www.sqlite.org/lang_returning.html 77b0217656Sgerardnico 78b0217656Sgerardnico $eventTableName = self::EVENT_TABLE_NAME; 79b0217656Sgerardnico $statement = "delete from {$eventTableName} returning *"; 80b0217656Sgerardnico // https://www.sqlite.org/lang_delete.html#optional_limit_and_order_by_clauses 81b0217656Sgerardnico if ($sqlite->hasOption("SQLITE_ENABLE_UPDATE_DELETE_LIMIT")) { 82b0217656Sgerardnico $statement .= "order by timestamp limit {$maxEvent}"; 83b0217656Sgerardnico } 84b0217656Sgerardnico $request = $sqlite->createRequest() 85b0217656Sgerardnico ->setStatement($statement); 86b0217656Sgerardnico try { 87b0217656Sgerardnico $rows = $request->execute() 88b0217656Sgerardnico ->getRows(); 89b0217656Sgerardnico if (sizeof($rows) === 0) { 90b0217656Sgerardnico return; 91b0217656Sgerardnico } 92b0217656Sgerardnico } catch (ExceptionCompile $e) { 93b0217656Sgerardnico LogUtility::error($e->getMessage(), $e->getCanonical(), $e); 94b0217656Sgerardnico } finally { 95b0217656Sgerardnico $request->close(); 96b0217656Sgerardnico } 97b0217656Sgerardnico 98b0217656Sgerardnico } 99c3437056SNickeau 100c3437056SNickeau /** 101c3437056SNickeau * Error in the block before or not the good version 102c3437056SNickeau * We try to get the records with a select/delete 103c3437056SNickeau */ 104c3437056SNickeau if (sizeof($rows) === 0) { 105c3437056SNickeau 106c3437056SNickeau 107c3437056SNickeau // technically the lock system of dokuwiki does not allow two process to run on 108c3437056SNickeau // the indexer, we trust it 109c3437056SNickeau $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID]; 110c3437056SNickeau $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes); 111b0217656Sgerardnico $select .= " order by " . self::TIMESTAMP_ATTRIBUTE . " limit {$maxEvent}"; 112c3437056SNickeau $request = $sqlite->createRequest() 113c3437056SNickeau ->setQuery($select); 114c3437056SNickeau 115c3437056SNickeau $rowsSelected = []; 116c3437056SNickeau try { 117c3437056SNickeau $rowsSelected = $request->execute() 118c3437056SNickeau ->getRows(); 119c3437056SNickeau if (sizeof($rowsSelected) === 0) { 120c3437056SNickeau return; 121c3437056SNickeau } 12204fd306cSNickeau } catch (ExceptionCompile $e) { 123c3437056SNickeau LogUtility::msg("Error while retrieving the event {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical()); 124c3437056SNickeau return; 125c3437056SNickeau } finally { 126c3437056SNickeau $request->close(); 127c3437056SNickeau } 128c3437056SNickeau 129c3437056SNickeau $eventTableName = self::EVENT_TABLE_NAME; 130c3437056SNickeau $rows = []; 131c3437056SNickeau foreach ($rowsSelected as $row) { 132c3437056SNickeau $request = $sqlite->createRequest() 133c3437056SNickeau ->setQueryParametrized("delete from $eventTableName where rowid = ? ", [$row[DatabasePageRow::ROWID]]); 134c3437056SNickeau try { 135b0217656Sgerardnico $changeCount = $request->execute()->getChangeCount(); 136c3437056SNickeau if ($changeCount !== 1) { 137c3437056SNickeau LogUtility::msg("The delete of the event was not successful or it was deleted by another process", LogUtility::LVL_MSG_ERROR); 138c3437056SNickeau } else { 139c3437056SNickeau $rows[] = $row; 140c3437056SNickeau } 14104fd306cSNickeau } catch (ExceptionCompile $e) { 142c3437056SNickeau LogUtility::msg("Error while deleting the event. Message {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical()); 143c3437056SNickeau return; 144c3437056SNickeau } finally { 145c3437056SNickeau $request->close(); 146c3437056SNickeau } 147c3437056SNickeau } 148c3437056SNickeau 149c3437056SNickeau 150c3437056SNickeau } 151c3437056SNickeau 152c3437056SNickeau 153c3437056SNickeau $eventCounter = 0; 154c3437056SNickeau foreach ($rows as $row) { 155c3437056SNickeau $eventCounter++; 156c3437056SNickeau $eventName = $row[self::EVENT_NAME_ATTRIBUTE]; 157c3437056SNickeau $eventData = []; 158c3437056SNickeau $eventDataJson = $row[self::EVENT_DATA_ATTRIBUTE]; 159c3437056SNickeau if ($eventDataJson !== null) { 160c3437056SNickeau try { 161c3437056SNickeau $eventData = Json::createFromString($eventDataJson)->toArray(); 16204fd306cSNickeau } catch (ExceptionCompile $e) { 163c3437056SNickeau LogUtility::msg("The stored data for the event $eventName was not in the json format"); 164c3437056SNickeau continue; 165c3437056SNickeau } 166c3437056SNickeau } 167c3437056SNickeau \dokuwiki\Extension\Event::createAndTrigger($eventName, $eventData); 168c3437056SNickeau 169c3437056SNickeau if ($eventCounter >= $maxEvent) { 170c3437056SNickeau break; 171c3437056SNickeau } 172c3437056SNickeau 173c3437056SNickeau } 1749fc13f00Sgerardnico } catch (\Exception $e) { 1759fc13f00Sgerardnico LogUtility::internalError("An internal error has runned on event. " . $e->getMessage(), self::CANONICAL, $e); 176a6d63b89Sgerardnico } finally { 177a6d63b89Sgerardnico $lock->release(); 178*4d15adccSNico print "$comboFunctionName(): Lock Released" . NL; 179a6d63b89Sgerardnico } 180c3437056SNickeau 181c3437056SNickeau } 182c3437056SNickeau 183c3437056SNickeau /** 184c3437056SNickeau * Ask a replication in the background 185c3437056SNickeau * @param string $name - a string with the reason 186c3437056SNickeau * @param array $data 187c3437056SNickeau */ 188c3437056SNickeau public static 189c3437056SNickeau function createEvent(string $name, array $data) 190c3437056SNickeau { 191c3437056SNickeau 19204fd306cSNickeau try { 193c3437056SNickeau $sqlite = Sqlite::createOrGetBackendSqlite(); 19404fd306cSNickeau } catch (ExceptionSqliteNotAvailable $e) { 19504fd306cSNickeau LogUtility::error("Unable to create the event $name. Sqlite is not available"); 196c3437056SNickeau return; 197c3437056SNickeau } 198c3437056SNickeau 199c3437056SNickeau /** 200c3437056SNickeau * If not present 201c3437056SNickeau */ 202c3437056SNickeau $entry = array( 203c3437056SNickeau "name" => $name, 204c3437056SNickeau "timestamp" => Iso8601Date::createFromNow()->toString() 205c3437056SNickeau ); 206c3437056SNickeau 20704fd306cSNickeau 208c3437056SNickeau $entry["data"] = Json::createFromArray($data)->toPrettyJsonString(); 20904fd306cSNickeau $entry["data_hash"] = md5($entry["data"]); 210c3437056SNickeau 211c3437056SNickeau /** 212c3437056SNickeau * Execute 213c3437056SNickeau */ 214c3437056SNickeau $request = $sqlite->createRequest() 215c3437056SNickeau ->setTableRow(self::EVENT_TABLE_NAME, $entry); 216c3437056SNickeau try { 217c3437056SNickeau $request->execute(); 21804fd306cSNickeau } catch (ExceptionCompile $e) { 21904fd306cSNickeau LogUtility::error("Unable to create the event $name. Error:" . $e->getMessage(), self::CANONICAL, $e); 220c3437056SNickeau } finally { 221c3437056SNickeau $request->close(); 222c3437056SNickeau } 223c3437056SNickeau 224c3437056SNickeau 225c3437056SNickeau } 226c3437056SNickeau 227c3437056SNickeau /** 228c3437056SNickeau * @param $pageId 229c3437056SNickeau * 230c3437056SNickeau * This is equivalent to {@link TaskRunner} 231c3437056SNickeau * 232c3437056SNickeau * lib/exe/taskrunner.php?id='.rawurlencode($ID) 233c3437056SNickeau * $taskRunner = new \dokuwiki\TaskRunner(); 234c3437056SNickeau * $taskRunner->run(); 235c3437056SNickeau * 236c3437056SNickeau */ 237c3437056SNickeau public static function startTaskRunnerForPage($pageId) 238c3437056SNickeau { 239c3437056SNickeau $tmp = []; // No event data 240c3437056SNickeau $tmp['page'] = $pageId; 241c3437056SNickeau $evt = new \dokuwiki\Extension\Event('INDEXER_TASKS_RUN', $tmp); 24204fd306cSNickeau $evt->advise_before(); 243c3437056SNickeau $evt->advise_after(); 244c3437056SNickeau } 245c3437056SNickeau 2464cadd4f8SNickeau 24704fd306cSNickeau public static function getQueue(string $eventName = null): array 24804fd306cSNickeau { 24904fd306cSNickeau try { 25004fd306cSNickeau $sqlite = Sqlite::createOrGetBackendSqlite(); 25104fd306cSNickeau } catch (ExceptionSqliteNotAvailable $e) { 25204fd306cSNickeau LogUtility::internalError("Sqlite is not available, no events was returned", self::CANONICAL); 25304fd306cSNickeau return []; 25404fd306cSNickeau } 2554cadd4f8SNickeau 2564cadd4f8SNickeau /** 2574cadd4f8SNickeau * Execute 2584cadd4f8SNickeau */ 2594cadd4f8SNickeau $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID]; 2604cadd4f8SNickeau $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes); 26104fd306cSNickeau $request = $sqlite->createRequest(); 26204fd306cSNickeau if (empty($eventName)) { 26304fd306cSNickeau $request->setQuery($select); 26404fd306cSNickeau } else { 26504fd306cSNickeau $request->setQueryParametrized($select . " where " . self::EVENT_NAME_ATTRIBUTE . " = ?", [$eventName]); 26604fd306cSNickeau } 2674cadd4f8SNickeau try { 2684cadd4f8SNickeau return $request->execute() 2694cadd4f8SNickeau ->getRows(); 27004fd306cSNickeau } catch (ExceptionCompile $e) { 27104fd306cSNickeau LogUtility::internalError("Unable to get the queue. Error:" . $e->getMessage(), self::CANONICAL, $e); 27204fd306cSNickeau return []; 2734cadd4f8SNickeau } finally { 2744cadd4f8SNickeau $request->close(); 2754cadd4f8SNickeau } 2764cadd4f8SNickeau 2774cadd4f8SNickeau } 2784cadd4f8SNickeau 2794cadd4f8SNickeau /** 28004fd306cSNickeau * @throws ExceptionCompile 2814cadd4f8SNickeau */ 2824cadd4f8SNickeau public static function purgeQueue(): int 2834cadd4f8SNickeau { 2844cadd4f8SNickeau $sqlite = Sqlite::createOrGetBackendSqlite(); 2854cadd4f8SNickeau if ($sqlite === null) { 28604fd306cSNickeau throw new ExceptionCompile("Sqlite is not available"); 287c3437056SNickeau } 288c3437056SNickeau 289c3437056SNickeau 290c3437056SNickeau /** 291c3437056SNickeau * Execute 292c3437056SNickeau */ 29304fd306cSNickeau /** @noinspection SqlWithoutWhere */ 294c3437056SNickeau $request = $sqlite->createRequest() 2954cadd4f8SNickeau ->setQuery("delete from " . self::EVENT_TABLE_NAME); 296c3437056SNickeau try { 2974cadd4f8SNickeau return $request->execute() 2984cadd4f8SNickeau ->getChangeCount(); 29904fd306cSNickeau } catch (ExceptionCompile $e) { 30004fd306cSNickeau throw new ExceptionCompile("Unable to count the number of event in the queue. Error:" . $e->getMessage(), self::CANONICAL, 0, $e); 301c3437056SNickeau } finally { 302c3437056SNickeau $request->close(); 303c3437056SNickeau } 304c3437056SNickeau } 305c3437056SNickeau 30604fd306cSNickeau /** 30704fd306cSNickeau * @throws ExceptionCompile 30804fd306cSNickeau */ 30904fd306cSNickeau public static function getEvents(string $eventName): array 31004fd306cSNickeau { 31104fd306cSNickeau return Event::getQueue($eventName); 31204fd306cSNickeau } 31304fd306cSNickeau 3140360a848Sgerardnico public static function getLock(): Lock 3150360a848Sgerardnico { 3160360a848Sgerardnico return Lock::create("combo-event"); 3170360a848Sgerardnico } 3180360a848Sgerardnico 319c3437056SNickeau 320c3437056SNickeau} 321